From 5509fcfbe6696167abeb8d3c3631a4d9f7393757 Mon Sep 17 00:00:00 2001 From: Guilherme Rodrigues Date: Thu, 29 Jan 2026 14:07:32 -0300 Subject: [PATCH] feat: Add Slides, Brand, Blog, Bookmarks, and Local-FS MCPs with shared 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 --- blog/README.md | 125 ++ blog/app.json | 26 + blog/bun.lock | 573 ++++++ blog/package.json | 32 + blog/server/main.ts | 135 ++ blog/server/prompts.ts | 658 +++++++ blog/server/resources/index.ts | 371 ++++ blog/server/serve.ts | 186 ++ blog/server/tools/filesystem.ts | 259 +++ blog/server/tools/index.ts | 273 +++ blog/server/types/env.ts | 22 + blog/tsconfig.json | 34 + bookmarks/README.md | 105 ++ bookmarks/app.json | 24 + bookmarks/bun.lock | 573 ++++++ bookmarks/package.json | 32 + bookmarks/server/main.ts | 133 ++ bookmarks/server/prompts.ts | 246 +++ bookmarks/server/serve.ts | 186 ++ bookmarks/server/tools/crud.ts | 419 +++++ bookmarks/server/tools/enrichment.ts | 400 ++++ bookmarks/server/tools/import.ts | 274 +++ bookmarks/server/tools/index.ts | 22 + bookmarks/server/types/env.ts | 23 + bookmarks/tsconfig.json | 34 + brand/README.md | 221 +++ brand/package.json | 29 + brand/server/main.ts | 121 ++ brand/server/resources/index.ts | 572 ++++++ brand/server/tools/generator-utils.ts | 266 +++ brand/server/tools/generator.ts | 816 ++++++++ brand/server/tools/index.ts | 16 + brand/server/tools/projects.ts | 695 +++++++ brand/server/tools/research.ts | 663 +++++++ brand/server/types/env.ts | 35 + brand/tsconfig.json | 17 + bun.lock | 519 ++---- local-fs/README.md | 196 ++ local-fs/bun.lock | 206 +++ local-fs/package.json | 59 + local-fs/server/cli.ts | 29 + local-fs/server/http.ts | 335 ++++ local-fs/server/logger.ts | 168 ++ local-fs/server/mcp.test.ts | 631 +++++++ local-fs/server/serve.ts | 215 +++ local-fs/server/stdio.ts | 82 + local-fs/server/storage.test.ts | 525 ++++++ local-fs/server/storage.ts | 487 +++++ local-fs/server/tools.ts | 1426 ++++++++++++++ local-fs/tsconfig.json | 16 + mcp-apps-testbed/README.md | 174 ++ mcp-apps-testbed/server/lib/resources.ts | 1185 ++++++++++++ mcp-apps-testbed/server/main.ts | 370 ++++ package.json | 12 +- shared/gateway/.gateway.env | 6 + shared/gateway/README.md | 117 ++ shared/gateway/package.json | 15 + shared/gateway/server.ts | 416 +++++ shared/gateway/setup.ts | 396 ++++ shared/registry.ts | 196 ++ slides/.gitignore | 1 + slides/README.md | 188 ++ slides/app.json | 19 + slides/assets/engine.js | 716 +++++++ slides/assets/index.html | 58 + slides/assets/manifest-template.json | 7 + slides/assets/slide-template.json | 13 + slides/assets/style-template.md | 52 + slides/assets/styles.css | 549 ++++++ slides/package.json | 32 + slides/server/main.ts | 118 ++ slides/server/prompts.ts | 653 +++++++ slides/server/resources/index.ts | 538 ++++++ slides/server/tools/deck.ts | 1642 +++++++++++++++++ slides/server/tools/index.ts | 26 + slides/server/tools/slides.ts | 597 ++++++ slides/server/tools/style.ts | 230 +++ slides/server/types/env.ts | 17 + slides/test-presentation/engine.js | 1154 ++++++++++++ slides/test-presentation/index.html | 22 + .../slides/001-introduction.json | 10 + .../slides/002-key-metrics.json | 15 + .../slides/003-features.json | 18 + .../slides/004-comparison.json | 30 + .../test-presentation/slides/005-quote.json | 9 + slides/test-presentation/slides/manifest.json | 38 + slides/test-presentation/styles.css | 217 +++ slides/tsconfig.json | 35 + 88 files changed, 23018 insertions(+), 383 deletions(-) create mode 100644 blog/README.md create mode 100644 blog/app.json create mode 100644 blog/bun.lock create mode 100644 blog/package.json create mode 100644 blog/server/main.ts create mode 100644 blog/server/prompts.ts create mode 100644 blog/server/resources/index.ts create mode 100644 blog/server/serve.ts create mode 100644 blog/server/tools/filesystem.ts create mode 100644 blog/server/tools/index.ts create mode 100644 blog/server/types/env.ts create mode 100644 blog/tsconfig.json create mode 100644 bookmarks/README.md create mode 100644 bookmarks/app.json create mode 100644 bookmarks/bun.lock create mode 100644 bookmarks/package.json create mode 100644 bookmarks/server/main.ts create mode 100644 bookmarks/server/prompts.ts create mode 100644 bookmarks/server/serve.ts create mode 100644 bookmarks/server/tools/crud.ts create mode 100644 bookmarks/server/tools/enrichment.ts create mode 100644 bookmarks/server/tools/import.ts create mode 100644 bookmarks/server/tools/index.ts create mode 100644 bookmarks/server/types/env.ts create mode 100644 bookmarks/tsconfig.json create mode 100644 brand/README.md create mode 100644 brand/package.json create mode 100644 brand/server/main.ts create mode 100644 brand/server/resources/index.ts create mode 100644 brand/server/tools/generator-utils.ts create mode 100644 brand/server/tools/generator.ts create mode 100644 brand/server/tools/index.ts create mode 100644 brand/server/tools/projects.ts create mode 100644 brand/server/tools/research.ts create mode 100644 brand/server/types/env.ts create mode 100644 brand/tsconfig.json create mode 100644 local-fs/README.md create mode 100644 local-fs/bun.lock create mode 100644 local-fs/package.json create mode 100644 local-fs/server/cli.ts create mode 100644 local-fs/server/http.ts create mode 100644 local-fs/server/logger.ts create mode 100644 local-fs/server/mcp.test.ts create mode 100755 local-fs/server/serve.ts create mode 100644 local-fs/server/stdio.ts create mode 100644 local-fs/server/storage.test.ts create mode 100644 local-fs/server/storage.ts create mode 100644 local-fs/server/tools.ts create mode 100644 local-fs/tsconfig.json create mode 100644 mcp-apps-testbed/README.md create mode 100644 mcp-apps-testbed/server/lib/resources.ts create mode 100644 mcp-apps-testbed/server/main.ts create mode 100644 shared/gateway/.gateway.env create mode 100644 shared/gateway/README.md create mode 100644 shared/gateway/package.json create mode 100644 shared/gateway/server.ts create mode 100644 shared/gateway/setup.ts create mode 100644 slides/.gitignore create mode 100644 slides/README.md create mode 100644 slides/app.json create mode 100644 slides/assets/engine.js create mode 100644 slides/assets/index.html create mode 100644 slides/assets/manifest-template.json create mode 100644 slides/assets/slide-template.json create mode 100644 slides/assets/style-template.md create mode 100644 slides/assets/styles.css create mode 100644 slides/package.json create mode 100644 slides/server/main.ts create mode 100644 slides/server/prompts.ts create mode 100644 slides/server/resources/index.ts create mode 100644 slides/server/tools/deck.ts create mode 100644 slides/server/tools/index.ts create mode 100644 slides/server/tools/slides.ts create mode 100644 slides/server/tools/style.ts create mode 100644 slides/server/types/env.ts create mode 100644 slides/test-presentation/engine.js create mode 100644 slides/test-presentation/index.html create mode 100644 slides/test-presentation/slides/001-introduction.json create mode 100644 slides/test-presentation/slides/002-key-metrics.json create mode 100644 slides/test-presentation/slides/003-features.json create mode 100644 slides/test-presentation/slides/004-comparison.json create mode 100644 slides/test-presentation/slides/005-quote.json create mode 100644 slides/test-presentation/slides/manifest.json create mode 100644 slides/test-presentation/styles.css create mode 100644 slides/tsconfig.json diff --git a/blog/README.md b/blog/README.md new file mode 100644 index 00000000..d3a7f4cd --- /dev/null +++ b/blog/README.md @@ -0,0 +1,125 @@ +# Blog MCP + +AI-powered blog writing assistant with tone of voice and visual style guides. + +## Overview + +The Blog MCP helps AI agents write blog articles with consistent voice and style. It provides: + +- **Prompts** for setting up blogs, creating style guides, and writing articles +- **Tools** for generating cover images and validating article structure +- **Resources** with templates for tone of voice and visual style guides + +## Key Concept + +Articles are stored as **markdown files with YAML frontmatter** in the git repository. The MCP guides the workflow - the agent reads/writes files directly. This enables: + +- Git-based version control for all content +- Easy integration with static site generators +- Future compatibility with deco.cx git-based editor + +## File Structure + +``` +project/ +└── blog/ # Blog folder + ├── tone-of-voice.md # Writing style guide + ├── visual-style.md # Image generation style guide + ├── config.json # Optional: default tags, author info + └── articles/ # Article markdown files + ├── my-first-post.md + └── another-article.md +``` + +## Article Format + +Each article is a markdown file with YAML frontmatter: + +```markdown +--- +slug: my-first-post +title: "My First Post" +description: "A brief description for SEO" +date: 2025-01-27 +status: draft +coverImage: /images/articles/my-first-post.png +tags: + - technology + - writing +--- + +Article content in markdown... +``` + +## Prompts + +| Prompt | Description | +|--------|-------------| +| `SETUP_PROJECT` | Initialize blog structure (blog/, blog/articles/) | +| `TONE_OF_VOICE_TEMPLATE` | Create a writing style guide | +| `VISUAL_STYLE_TEMPLATE` | Create a visual style guide for images | +| `WRITE_ARTICLE` | Workflow for writing new articles | +| `EDIT_ARTICLE` | Workflow for editing existing articles | + +## Tools + +### Helpers + +| Tool | Description | +|------|-------------| +| `COVER_IMAGE_GENERATE` | Generate cover image using IMAGE_GENERATOR binding | +| `ARTICLE_FRONTMATTER` | Generate valid YAML frontmatter for an article | +| `ARTICLE_VALIDATE` | Validate article structure and frontmatter | + +### Filesystem (requires LOCAL_FS binding) + +| Tool | Description | +|------|-------------| +| `BLOG_READ_STYLE_GUIDE` | Read tone-of-voice.md or visual-style.md | +| `BLOG_LIST_ARTICLES` | List all articles in blog/articles/ | +| `BLOG_READ_ARTICLE` | Read an article by slug | +| `BLOG_WRITE_ARTICLE` | Write/create an article | +| `BLOG_DELETE_ARTICLE` | Delete an article | + +## Resources + +| Resource | Description | +|----------|-------------| +| `resource://tone-of-voice-template` | Template for creating tone of voice guides | +| `resource://visual-style-template` | Template for creating visual style guides | + +## Bindings + +| Binding | Required | Description | +|---------|----------|-------------| +| `LOCAL_FS` | Optional | Local filesystem - select a folder with a `blog/` subfolder | +| `IMAGE_GENERATOR` | Optional | Image generation (nanobanana) for cover images | + +When `LOCAL_FS` is connected, the blog MCP becomes fully self-contained and can read/write articles directly. + +## Quick Start + +1. **Setup project**: Run `SETUP_PROJECT` prompt +2. **Create tone of voice**: Run `TONE_OF_VOICE_TEMPLATE` prompt +3. **Create visual style**: Run `VISUAL_STYLE_TEMPLATE` prompt +4. **Write articles**: Run `WRITE_ARTICLE` prompt + +## Development + +```bash +# Install dependencies +bun install + +# Run locally +bun run dev + +# Type check +bun run check + +# Build for production +bun run build +``` + +## License + +MIT diff --git a/blog/app.json b/blog/app.json new file mode 100644 index 00000000..a5d3b632 --- /dev/null +++ b/blog/app.json @@ -0,0 +1,26 @@ +{ + "scopeName": "deco", + "name": "blog", + "friendlyName": "Blog", + "connection": { + "type": "HTTP", + "url": "https://sites-blog.decocache.com/mcp" + }, + "description": "AI-powered blog writing assistant with tone of voice and visual style guides.", + "icon": "https://assets.decocache.com/decocms/blog-icon.png", + "unlisted": false, + "bindings": { + "OBJECT_STORAGE": { + "binding": "@deco/object-storage", + "description": "Select a folder containing a blog/ subfolder with articles/, tone-of-voice.md, and visual-style.md" + }, + "IMAGE_GENERATOR": "@deco/nanobanana" + }, + "metadata": { + "categories": ["Productivity", "Content"], + "official": false, + "tags": ["blog", "writing", "articles", "tone-of-voice", "markdown"], + "short_description": "Write blog articles with consistent voice and style.", + "mesh_description": "The Blog MCP helps AI agents write blog articles with consistent tone of voice and visual style. It provides prompts for setting up a blog project, creating tone of voice guides, visual style guides for cover images, and workflows for writing and editing articles. Articles are stored as markdown files with YAML frontmatter in the git repository. The MCP includes tools for generating cover images and validating article structure. Ideal for personal blogs, company blogs, or any content that requires consistent voice and style." + } +} diff --git a/blog/bun.lock b/blog/bun.lock new file mode 100644 index 00000000..c0f1d9c5 --- /dev/null +++ b/blog/bun.lock @@ -0,0 +1,573 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "@decocms/blog", + "dependencies": { + "@decocms/bindings": "^1.0.9", + "@decocms/runtime": "1.2.0", + "zod": "^4.0.0", + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "1.25.1", + "deco-cli": "^0.28.0", + "typescript": "^5.7.2", + }, + }, + }, + "packages": { + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.5", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w=="], + + "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260127.0", "", {}, "sha512-4M1HLcWViSdT/pAeDGEB5x5P3sqW7UIi34QrBRnxXbqjAY9if8vBU/lWRWnM+UqKzxWGB2LYjEVOzZrp0jZL+w=="], + + "@deco-cx/warp-node": ["@deco-cx/warp-node@0.3.16", "", { "dependencies": { "undici": "^6.21.0", "ws": "^8.18.0" } }, "sha512-8cak+6YDrfJiYAkRqLCcywXrDaCkfKjbq/zU0zYUc5DSTt5bOzrA7RifqCLAfAgtEBw0mDdcr4IRPqGz65RdbA=="], + + "@decocms/bindings": ["@decocms/bindings@1.1.3", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.2", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-G7GvtGhXa/LoPllCVaeGAnkM9Mz5bG/bmUEfdno9+flKTIWAm9HUqSACX48jRv25OiAir6KbL1JX/xF6uHSUtQ=="], + + "@decocms/runtime": ["@decocms/runtime@1.2.0", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-HkhKBrCGYPlKyYW+iZxl7hmSwvzeJbBkFYE1+/0y5Ok1rlf/pxHHBHfiFlVgs1pr/seyFjEsqi2LnK8bMnDOEg=="], + + "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@supabase/auth-js": ["@supabase/auth-js@2.70.0", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg=="], + + "@supabase/functions-js": ["@supabase/functions-js@2.4.4", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA=="], + + "@supabase/node-fetch": ["@supabase/node-fetch@2.6.15", "", { "dependencies": { "whatwg-url": "^5.0.0" } }, "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ=="], + + "@supabase/postgrest-js": ["@supabase/postgrest-js@1.19.4", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw=="], + + "@supabase/realtime-js": ["@supabase/realtime-js@2.11.10", "", { "dependencies": { "@supabase/node-fetch": "^2.6.13", "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "ws": "^8.18.2" } }, "sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA=="], + + "@supabase/ssr": ["@supabase/ssr@0.6.1", "", { "dependencies": { "cookie": "^1.0.1" }, "peerDependencies": { "@supabase/supabase-js": "^2.43.4" } }, "sha512-QtQgEMvaDzr77Mk3vZ3jWg2/y+D8tExYF7vcJT+wQ8ysuvOeGGjYbZlvj5bHYsj/SpC0bihcisnwPrM4Gp5G4g=="], + + "@supabase/storage-js": ["@supabase/storage-js@2.7.1", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA=="], + + "@supabase/supabase-js": ["@supabase/supabase-js@2.50.0", "", { "dependencies": { "@supabase/auth-js": "2.70.0", "@supabase/functions-js": "2.4.4", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.19.4", "@supabase/realtime-js": "2.11.10", "@supabase/storage-js": "2.7.1" } }, "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg=="], + + "@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], + + "@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], + + "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="], + + "@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], + + "@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="], + + "@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], + + "@types/phoenix": ["@types/phoenix@1.6.7", "", {}, "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deco-cli": ["deco-cli@0.28.6", "", { "dependencies": { "@deco-cx/warp-node": "0.3.16", "@modelcontextprotocol/sdk": "1.25.1", "@supabase/ssr": "0.6.1", "@supabase/supabase-js": "2.50.0", "chalk": "^5.3.0", "commander": "^12.0.0", "glob": "^10.3.10", "ignore": "^7.0.5", "inquirer": "^9.2.15", "inquirer-search-checkbox": "^1.0.0", "inquirer-search-list": "^1.2.6", "jose": "^6.0.11", "json-schema-to-typescript": "^15.0.4", "object-hash": "^3.0.0", "prettier": "^3.6.2", "semver": "^7.6.0", "smol-toml": "^1.3.4", "zod": "^3.25.76" }, "bin": { "deco": "dist/cli.js", "deconfig": "dist/deconfig.js" } }, "sha512-IwdfHoZfrLVGTVULBJ2NRjEkD9dZafJSf3qYsZeer7CR5owQ1XLnDAKIwd/c6iwLZB6+2zrMjL4RNWhF2SzZbw=="], + + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], + + "external-editor": ["external-editor@2.2.0", "", { "dependencies": { "chardet": "^0.4.0", "iconv-lite": "^0.4.17", "tmp": "^0.0.33" } }, "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "figures": ["figures@2.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "fuzzy": ["fuzzy@0.1.3", "", {}, "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "inquirer": ["inquirer@9.3.8", "", { "dependencies": { "@inquirer/external-editor": "^1.0.2", "@inquirer/figures": "^1.0.3", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "1.0.0", "ora": "^5.4.1", "run-async": "^3.0.0", "rxjs": "^7.8.1", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" } }, "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w=="], + + "inquirer-search-checkbox": ["inquirer-search-checkbox@1.0.0", "", { "dependencies": { "chalk": "^2.3.0", "figures": "^2.0.0", "fuzzy": "^0.1.3", "inquirer": "^3.3.0" } }, "sha512-KR6kfe0+h7Zgyrj6GCBVgS4ZmmBhsXofcJoQv6EXZWxK+bpJZV9kOb2AaQ2fbjnH91G0tZWQaS5WteWygzXcmA=="], + + "inquirer-search-list": ["inquirer-search-list@1.2.6", "", { "dependencies": { "chalk": "^2.3.0", "figures": "^2.0.0", "fuzzy": "^0.1.3", "inquirer": "^3.3.0" } }, "sha512-C4pKSW7FOYnkAloH8rB4FiM91H1v08QFZZJh6KRt//bMfdDBIhgdX8wjHvrVH2bu5oIo6wYqGpzSBxkeClPxew=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + + "isbot": ["isbot@5.1.34", "", {}, "sha512-aCMIBSKd/XPRYdiCQTLC8QHH4YT8B3JUADu+7COgYIZPvkeoMcUHMRjZLM9/7V8fCj+l7FSREc1lOPNjzogo/A=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + + "json-schema-to-typescript": ["json-schema-to-typescript@15.0.4", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.5.5", "@types/json-schema": "^7.0.15", "@types/lodash": "^4.17.7", "is-glob": "^4.0.3", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "minimist": "^1.2.8", "prettier": "^3.2.5", "tinyglobby": "^0.2.9" }, "bin": { "json2ts": "dist/src/cli.js" } }, "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "run-async": ["run-async@3.0.0", "", {}, "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q=="], + + "rx-lite": ["rx-lite@4.0.8", "", {}, "sha512-Cun9QucwK6MIrp3mry/Y7hqD1oFqTYLQ4pGxaHTjIdaFDWRGGLikqp6u8LcWJnzpoALg9hap+JGk8sFIUuEGNA=="], + + "rx-lite-aggregates": ["rx-lite-aggregates@4.0.8", "", { "dependencies": { "rx-lite": "*" } }, "sha512-3xPNZGW93oCjiO7PtKxRK6iOVYBWBvtf9QHDfU23Oc+dLIQmAV//UnyXV/yihv81VS/UqoQPk4NegS8EFi55Hg=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="], + + "seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "zod-from-json-schema": ["zod-from-json-schema@0.5.2", "", { "dependencies": { "zod": "^4.0.17" } }, "sha512-/dNaicfdhJTOuUd4RImbLUE2g5yrSzzDjI/S6C2vO2ecAGZzn9UcRVgtyLSnENSmAOBRiSpUdzDS6fDWX3Z35g=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + + "@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@supabase/ssr/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "deco-cli/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "external-editor/chardet": ["chardet@0.4.2", "", {}, "sha512-j/Toj7f1z98Hh2cYo2BVr85EpIRWqUi7rtRSGxh/cqUjqrnJe9l9UE7IUGd2vQ2p+kSHLkSzObQPZPLUC6TQwg=="], + + "external-editor/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "inquirer-search-checkbox/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "inquirer-search-checkbox/inquirer": ["inquirer@3.3.0", "", { "dependencies": { "ansi-escapes": "^3.0.0", "chalk": "^2.0.0", "cli-cursor": "^2.1.0", "cli-width": "^2.0.0", "external-editor": "^2.0.4", "figures": "^2.0.0", "lodash": "^4.3.0", "mute-stream": "0.0.7", "run-async": "^2.2.0", "rx-lite": "^4.0.8", "rx-lite-aggregates": "^4.0.8", "string-width": "^2.1.0", "strip-ansi": "^4.0.0", "through": "^2.3.6" } }, "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ=="], + + "inquirer-search-list/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "inquirer-search-list/inquirer": ["inquirer@3.3.0", "", { "dependencies": { "ansi-escapes": "^3.0.0", "chalk": "^2.0.0", "cli-cursor": "^2.1.0", "cli-width": "^2.0.0", "external-editor": "^2.0.4", "figures": "^2.0.0", "lodash": "^4.3.0", "mute-stream": "0.0.7", "run-async": "^2.2.0", "rx-lite": "^4.0.8", "rx-lite-aggregates": "^4.0.8", "string-width": "^2.1.0", "strip-ansi": "^4.0.0", "through": "^2.3.6" } }, "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ=="], + + "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "inquirer-search-checkbox/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "inquirer-search-checkbox/inquirer/ansi-escapes": ["ansi-escapes@3.2.0", "", {}, "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ=="], + + "inquirer-search-checkbox/inquirer/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], + + "inquirer-search-checkbox/inquirer/cli-width": ["cli-width@2.2.1", "", {}, "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw=="], + + "inquirer-search-checkbox/inquirer/mute-stream": ["mute-stream@0.0.7", "", {}, "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ=="], + + "inquirer-search-checkbox/inquirer/run-async": ["run-async@2.4.1", "", {}, "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="], + + "inquirer-search-checkbox/inquirer/string-width": ["string-width@2.1.1", "", { "dependencies": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" } }, "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw=="], + + "inquirer-search-checkbox/inquirer/strip-ansi": ["strip-ansi@4.0.0", "", { "dependencies": { "ansi-regex": "^3.0.0" } }, "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow=="], + + "inquirer-search-list/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "inquirer-search-list/inquirer/ansi-escapes": ["ansi-escapes@3.2.0", "", {}, "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ=="], + + "inquirer-search-list/inquirer/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], + + "inquirer-search-list/inquirer/cli-width": ["cli-width@2.2.1", "", {}, "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw=="], + + "inquirer-search-list/inquirer/mute-stream": ["mute-stream@0.0.7", "", {}, "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ=="], + + "inquirer-search-list/inquirer/run-async": ["run-async@2.4.1", "", {}, "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="], + + "inquirer-search-list/inquirer/string-width": ["string-width@2.1.1", "", { "dependencies": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" } }, "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw=="], + + "inquirer-search-list/inquirer/strip-ansi": ["strip-ansi@4.0.0", "", { "dependencies": { "ansi-regex": "^3.0.0" } }, "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow=="], + + "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "ora/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "inquirer-search-checkbox/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "inquirer-search-checkbox/inquirer/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], + + "inquirer-search-checkbox/inquirer/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], + + "inquirer-search-checkbox/inquirer/strip-ansi/ansi-regex": ["ansi-regex@3.0.1", "", {}, "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw=="], + + "inquirer-search-list/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "inquirer-search-list/inquirer/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], + + "inquirer-search-list/inquirer/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], + + "inquirer-search-list/inquirer/strip-ansi/ansi-regex": ["ansi-regex@3.0.1", "", {}, "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw=="], + + "log-symbols/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "ora/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "inquirer-search-checkbox/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "inquirer-search-checkbox/inquirer/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], + + "inquirer-search-checkbox/inquirer/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "inquirer-search-list/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "inquirer-search-list/inquirer/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], + + "inquirer-search-list/inquirer/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "inquirer-search-checkbox/inquirer/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], + + "inquirer-search-list/inquirer/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], + } +} diff --git a/blog/package.json b/blog/package.json new file mode 100644 index 00000000..778d8658 --- /dev/null +++ b/blog/package.json @@ -0,0 +1,32 @@ +{ + "name": "@decocms/blog", + "version": "1.0.0", + "description": "AI-powered blog writing assistant with tone of voice and visual style guides", + "private": true, + "type": "module", + "scripts": { + "dev": "bun run --hot server/main.ts", + "serve": "bun run server/serve.ts", + "dev:link": "deco link -p 8010 -- PORT=8010 bun run dev", + "build:server": "NODE_ENV=production bun build server/main.ts --target=bun --outfile=dist/server/main.js", + "build": "bun run build:server", + "publish": "cat app.json | deco registry publish -w /shared/deco -y", + "check": "tsc --noEmit" + }, + "exports": { + "./tools": "./server/tools/index.ts" + }, + "dependencies": { + "@decocms/bindings": "^1.0.9", + "@decocms/runtime": "1.2.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "1.25.1", + "deco-cli": "^0.28.0", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/blog/server/main.ts b/blog/server/main.ts new file mode 100644 index 00000000..2eb57b0c --- /dev/null +++ b/blog/server/main.ts @@ -0,0 +1,135 @@ +/** + * Blog MCP - AI-Powered Blog Writing Assistant + * + * Helps agents write blog articles with consistent tone of voice and visual style. + * + * ## Features + * + * - **Tone of Voice Guides** - Create and use consistent writing voice + * - **Visual Style Guides** - Define image generation style for cover images + * - **Article Workflows** - Prompts for writing and editing articles + * - **Cover Image Generation** - Generate cover images via IMAGE_GENERATOR binding + * + * ## Article Storage + * + * Articles are stored as markdown files with YAML frontmatter in the git repository. + * The MCP guides the workflow - the agent reads/writes files directly. + */ +import { withRuntime } from "@decocms/runtime"; +import { tools } from "./tools/index.ts"; +import { prompts } from "./prompts.ts"; +import { resources } from "./resources/index.ts"; +import { StateSchema, type Env } from "./types/env.ts"; +import type { BindingRegistry } from "@decocms/runtime"; + +export { StateSchema }; + +type Registry = BindingRegistry; + +const PORT = process.env.PORT || 8005; + +console.log("[blog-mcp] Starting server..."); +console.log("[blog-mcp] Port:", PORT); +console.log("[blog-mcp] Tools count:", tools.length); +console.log("[blog-mcp] Prompts count:", prompts.length); +console.log("[blog-mcp] Resources count:", resources.length); + +const runtime = withRuntime({ + configuration: { + scopes: ["IMAGE_GENERATOR::*"], + state: StateSchema, + }, + tools, + prompts, + resources, +}); + +console.log("[blog-mcp] Runtime initialized"); + +/** + * Fetch handler with logging + */ +const fetchWithLogging = async (req: Request): Promise => { + const url = new URL(req.url); + const startTime = Date.now(); + + if (req.method === "POST" && url.pathname === "/mcp") { + try { + const body = await req.clone().json(); + const method = body?.method || "unknown"; + const toolName = body?.params?.name; + + if (method === "tools/call" && toolName) { + console.log(`[blog-mcp] 🔧 Tool call: ${toolName}`); + } else if (method !== "unknown") { + console.log(`[blog-mcp] 📨 Request: ${method}`); + } + } catch { + // Ignore JSON parse errors + } + } + + // biome-ignore lint/suspicious/noExplicitAny: runtime.fetch type is complex + const response = await (runtime.fetch as any)(req); + + const duration = Date.now() - startTime; + if (duration > 100) { + console.log(`[blog-mcp] ⏱️ Response in ${duration}ms`); + } + + return response; +}; + +// Start the server +Bun.serve({ + port: PORT, + hostname: "0.0.0.0", + idleTimeout: 0, + fetch: fetchWithLogging, + development: process.env.NODE_ENV !== "production", +}); + +console.log(""); +console.log("📝 Blog MCP running at: http://localhost:" + PORT + "/mcp"); +console.log(""); +console.log("[blog-mcp] Available prompts:"); +console.log(" - SETUP_PROJECT - Initialize blog structure"); +console.log(" - TONE_OF_VOICE_TEMPLATE - Create tone of voice guide"); +console.log(" - VISUAL_STYLE_TEMPLATE - Create visual style guide"); +console.log(" - WRITE_ARTICLE - Workflow for writing articles"); +console.log(" - EDIT_ARTICLE - Workflow for editing articles"); +console.log(""); +console.log("[blog-mcp] Available tools:"); +console.log(" Helpers:"); +console.log(" - COVER_IMAGE_GENERATE - Generate cover image"); +console.log(" - ARTICLE_FRONTMATTER - Generate article frontmatter"); +console.log(" - ARTICLE_VALIDATE - Validate article structure"); +console.log(""); +console.log(" Filesystem (requires OBJECT_STORAGE):"); +console.log(" - BLOG_READ_STYLE_GUIDE - Read tone/visual style guide"); +console.log(" - BLOG_LIST_ARTICLES - List all articles"); +console.log(" - BLOG_READ_ARTICLE - Read an article"); +console.log(" - BLOG_WRITE_ARTICLE - Write an article"); +console.log(" - BLOG_DELETE_ARTICLE - Delete an article"); +console.log(""); +console.log("[blog-mcp] Resources:"); +console.log(" - resource://tone-of-voice-template"); +console.log(" - resource://visual-style-template"); +console.log(""); +console.log("[blog-mcp] Bindings:"); +console.log( + " - OBJECT_STORAGE (@deco/object-storage) - Folder with blog/ subfolder", +); +console.log(" - IMAGE_GENERATOR (@deco/nanobanana) - Cover image generation"); + +// Copy URL to clipboard on macOS +if (process.platform === "darwin") { + try { + const proc = Bun.spawn(["pbcopy"], { stdin: "pipe" }); + proc.stdin.write(`http://localhost:${PORT}/mcp`); + proc.stdin.end(); + console.log("[blog-mcp] 📋 MCP URL copied to clipboard!"); + } catch { + // Ignore clipboard errors + } +} diff --git a/blog/server/prompts.ts b/blog/server/prompts.ts new file mode 100644 index 00000000..2bdd5a87 --- /dev/null +++ b/blog/server/prompts.ts @@ -0,0 +1,658 @@ +/** + * Blog MCP Prompts + * + * These prompts guide the workflow for writing blog articles with consistent + * tone of voice and visual style. + * + * ## File Structure + * + * project/ + * ├── blog/ # Blog configuration + * │ ├── tone-of-voice.md # Writing style guide + * │ ├── visual-style.md # Image generation style guide + * │ └── config.json # Optional: default tags, author info + * └── data/ + * └── articles/ # Article markdown files + * ├── my-first-post.md + * └── another-article.md + * + * ## Workflow + * + * 1. SETUP_PROJECT - Initialize blog structure (one-time) + * 2. TONE_OF_VOICE_TEMPLATE - Create tone of voice guide + * 3. VISUAL_STYLE_TEMPLATE - Create visual style guide + * 4. WRITE_ARTICLE - Write new articles + * 5. EDIT_ARTICLE - Edit existing articles + */ + +import { createPrompt, type GetPromptResult } from "@decocms/runtime"; +import { z } from "zod"; +import type { Env } from "./types/env.ts"; + +/** + * SETUP_PROJECT - Initialize blog structure in a project + */ +export const createSetupProjectPrompt = (_env: Env) => + createPrompt({ + name: "SETUP_PROJECT", + title: "Setup Blog Project", + description: `Initialize a blog project structure with configuration folders. + +This is the FIRST step for a new blog. Creates: +- blog/ folder for configuration +- blog/articles/ folder for article markdown files + +After setup, use TONE_OF_VOICE_TEMPLATE and VISUAL_STYLE_TEMPLATE to create style guides.`, + argsSchema: { + projectPath: z + .string() + .optional() + .describe("Project root path (default: current directory)"), + }, + execute: ({ args }): GetPromptResult => { + const projectPath = args.projectPath || "."; + + return { + description: "Initialize blog project structure", + messages: [ + { + role: "user", + content: { + type: "text", + text: `# Setup Blog Project + +## Task +Initialize the blog structure in: ${projectPath} + +## Step 1: Create directories + +\`\`\`bash +mkdir -p ${projectPath}/blog +mkdir -p ${projectPath}/blog/articles +\`\`\` + +## Step 2: Create placeholder config + +Write to \`${projectPath}/blog/config.json\`: +\`\`\`json +{ + "author": "Your Name", + "defaultTags": [], + "imageStyle": "blog/visual-style.md" +} +\`\`\` + +## Step 3: Verify structure + +\`\`\`bash +ls -la ${projectPath}/blog/ +ls -la ${projectPath}/blog/articles/ +\`\`\` + +## Next Steps + +Tell the user: +"✓ Blog structure created! + +Next, let's set up your writing style: +1. Run **TONE_OF_VOICE_TEMPLATE** to create your unique writing voice +2. Run **VISUAL_STYLE_TEMPLATE** to define your visual style for cover images + +Would you like to start with the tone of voice guide?"`, + }, + }, + ], + }; + }, + }); + +/** + * TONE_OF_VOICE_TEMPLATE - Guide for creating tone of voice + */ +export const createToneOfVoicePrompt = (_env: Env) => + createPrompt({ + name: "TONE_OF_VOICE_TEMPLATE", + title: "Create Tone of Voice Guide", + description: `Create a comprehensive tone of voice guide for writing articles. + +This prompt helps extract your unique writing voice by: +1. Analyzing existing content (if available) +2. Asking key questions about your style +3. Generating a detailed writing guide + +The guide is saved to blog/tone-of-voice.md and referenced when writing articles.`, + argsSchema: { + existingContent: z + .string() + .optional() + .describe("URLs or paths to existing articles to analyze"), + authorName: z.string().optional().describe("Author name for the guide"), + projectPath: z + .string() + .optional() + .describe("Project root path (default: current directory)"), + }, + execute: ({ args }): GetPromptResult => { + const projectPath = args.projectPath || "."; + const authorName = args.authorName || "the author"; + + return { + description: `Create tone of voice guide for ${authorName}`, + messages: [ + { + role: "user", + content: { + type: "text", + text: `# Create Tone of Voice Guide + +## Goal +Create a comprehensive tone of voice guide for ${authorName} and save it to: +\`${projectPath}/blog/tone-of-voice.md\` + +${ + args.existingContent + ? `## Analyze Existing Content + +First, read and analyze these sources: +${args.existingContent} + +Extract patterns for: +- Hook styles (how articles open) +- Sentence rhythm (short vs long) +- Vocabulary preferences +- Emotional register +- Signature phrases +` + : "" +} + +## Interview Questions + +Ask the user these questions to understand their voice: + +### 1. Core Identity +- "How would you describe your writing in 3 words?" +- "Who are you writing for? (audience)" +- "What makes your perspective unique?" + +### 2. Style Preferences +- "Do you prefer formal or conversational tone?" +- "Short punchy sentences or longer explanatory ones?" +- "Do you use first person (I) or avoid it?" + +### 3. Influences +- "Which writers or thinkers influence you?" +- "Any books or philosophies that shape your worldview?" + +### 4. Emotional Register +- "How vulnerable do you get in writing?" +- "Do you use humor? What kind?" +- "How do you handle disagreement or controversy?" + +## Guide Structure + +After gathering info, create a markdown file with these sections: + +\`\`\`markdown +# [Author Name]'s Tone of Voice Guide + +## Core Identity +[Who they are as a writer, primary archetype] + +## Voice DNA +- [Key trait 1] +- [Key trait 2] +- [Key trait 3] + +## Hook Patterns +[How to open articles - with examples] + +## Sentence Rhythm +[Short vs long, paragraph structure] + +## Vocabulary +### Words to use: +- [word 1] - [when/why] +- [word 2] - [when/why] + +### Words to avoid: +- [word 1] - [why] +- [word 2] - [why] + +## Emotional Register +[How much vulnerability, intensity level] + +## Philosophical Framework +[Key influences, worldview] + +## Implementation Checklist +- [ ] Opening hook stops the scroll +- [ ] At least one vulnerable moment +- [ ] Connects philosophy to practice +- [ ] Ends with invitation or grounded statement + +## Quick Reference +\`\`\` +ESSENCE: [One-line summary] +HOOKS: [Types of openings] +RHYTHM: [Sentence style] +CLOSE: [How to end] +AVOID: [What not to do] +\`\`\` +\`\`\` + +## Save the Guide + +Write the completed guide to: +\`${projectPath}/blog/tone-of-voice.md\` + +## Completion + +Tell the user: +"✓ Tone of voice guide saved to blog/tone-of-voice.md + +I'll reference this guide when writing articles. You can edit it anytime to refine your voice. + +Would you like to create a visual style guide for cover images next?"`, + }, + }, + ], + }; + }, + }); + +/** + * VISUAL_STYLE_TEMPLATE - Guide for creating visual style + */ +export const createVisualStylePrompt = (_env: Env) => + createPrompt({ + name: "VISUAL_STYLE_TEMPLATE", + title: "Create Visual Style Guide", + description: `Create a visual style guide for generating cover images. + +This guide defines: +- Color palette +- Aesthetic style (retro, modern, minimalist, etc.) +- Image effects (dithering, gradients, etc.) +- Prompt templates for image generation + +The guide is saved to blog/visual-style.md and used by COVER_IMAGE_GENERATE.`, + argsSchema: { + aesthetic: z + .string() + .optional() + .describe("Desired aesthetic (e.g., 'retro comic', 'minimalist')"), + primaryColor: z + .string() + .optional() + .describe("Primary brand color (hex, e.g., '#1a4d3e')"), + accentColor: z + .string() + .optional() + .describe("Accent color (hex, e.g., '#c4e538')"), + projectPath: z + .string() + .optional() + .describe("Project root path (default: current directory)"), + }, + execute: ({ args }): GetPromptResult => { + const projectPath = args.projectPath || "."; + + return { + description: "Create visual style guide for cover images", + messages: [ + { + role: "user", + content: { + type: "text", + text: `# Create Visual Style Guide + +## Goal +Create a visual style guide for cover images and save it to: +\`${projectPath}/blog/visual-style.md\` + +## Questions to Ask + +### 1. Color Palette +${ + args.primaryColor + ? `- Primary color provided: ${args.primaryColor}` + : '- "What is your primary/background color?"' +} +${ + args.accentColor + ? `- Accent color provided: ${args.accentColor}` + : '- "What is your accent/highlight color?"' +} +- "Should images be monochromatic or use multiple colors?" + +### 2. Aesthetic Style +${ + args.aesthetic + ? `- Aesthetic provided: ${args.aesthetic}` + : `- "What visual style do you prefer?" + Examples: + - Retro 1950s comic book + - Modern minimalist + - Cyberpunk/neon + - Watercolor illustration + - Abstract geometric + - Photography-based` +} + +### 3. Effects & Textures +- "Any specific effects? (dithering, gradients, grain, halftone)" +- "Flat design or textured?" + +### 4. Subjects & Imagery +- "What kind of subjects? (abstract, people, objects, scenes)" +- "Any imagery to avoid?" + +## Guide Structure + +Create a markdown file with: + +\`\`\`markdown +# Visual Style Guide + +## Overview +[Brief description of the visual identity] + +## Color Palette + +| Role | Hex | Description | +|------|-----|-------------| +| Background | #XXXXXX | [description] | +| Accent | #XXXXXX | [description] | + +## Aesthetic Style +[Description of the style, influences, references] + +## Visual Elements +### Style Influences +- [Influence 1] +- [Influence 2] + +### Required Effects +- [Effect 1: e.g., "heavy dithering"] +- [Effect 2: e.g., "halftone dots"] + +### Composition +- [Guidance on composition] + +## Image Generation Prompt Template + +\`\`\` +[Base prompt template with placeholders for CONCEPT, DIMENSIONS] +\`\`\` + +## Example Prompts + +### Article Header (1200x630) +\`\`\` +[Complete example prompt] +\`\`\` + +### Square Social (1080x1080) +\`\`\` +[Complete example prompt] +\`\`\` + +## What to Avoid +- ❌ [Thing to avoid 1] +- ❌ [Thing to avoid 2] + +## Concept Adaptations + +| Concept | Visual Treatment | +|---------|-----------------| +| Technology | [How to represent] | +| Leadership | [How to represent] | +| Growth | [How to represent] | +\`\`\` + +## Save the Guide + +Write the completed guide to: +\`${projectPath}/blog/visual-style.md\` + +## Completion + +Tell the user: +"✓ Visual style guide saved to blog/visual-style.md + +When generating cover images with COVER_IMAGE_GENERATE, I'll use these guidelines. + +Your blog setup is complete! You can now write articles with WRITE_ARTICLE."`, + }, + }, + ], + }; + }, + }); + +/** + * WRITE_ARTICLE - Workflow for writing a new article + */ +export const createWriteArticlePrompt = (_env: Env) => + createPrompt({ + name: "WRITE_ARTICLE", + title: "Write New Article", + description: `Workflow for writing a new blog article. + +This prompt: +1. Instructs the agent to read blog/tone-of-voice.md first +2. Provides article structure guidelines +3. Shows the frontmatter format +4. Guides saving to blog/articles/{slug}.md`, + argsSchema: { + topic: z.string().describe("Topic or title of the article"), + notes: z + .string() + .optional() + .describe("Notes, outline, or key points to cover"), + projectPath: z + .string() + .optional() + .describe("Project root path (default: current directory)"), + }, + execute: ({ args }): GetPromptResult => { + const projectPath = args.projectPath || "."; + const { topic, notes } = args; + + return { + description: `Write article about: ${topic}`, + messages: [ + { + role: "user", + content: { + type: "text", + text: `# Write New Article: ${topic} + +## Step 1: Read Tone of Voice Guide + +**IMPORTANT**: Before writing, read the tone of voice guide: +\`${projectPath}/blog/tone-of-voice.md\` + +Apply these guidelines throughout the article. + +## Step 2: Plan the Article + +${ + notes + ? `### Notes Provided: +${notes} +` + : "" +} + +### Article Structure (from tone guide): +1. **Hook** (1-2 sentences) - Stop the scroll +2. **Development** (3-5 paragraphs) - Build the argument +3. **Turn/Insight** (1-2 sentences) - The "aha" moment +4. **Close** (1-2 sentences) - Challenge, quote, or grounded statement + +## Step 3: Generate Slug + +Convert the title to a URL-friendly slug: +- Lowercase +- Replace spaces with hyphens +- Remove special characters +- Keep it concise + +Example: "Why I Love Mondays" → "why-i-love-mondays" + +## Step 4: Write the Article + +Create the article with YAML frontmatter: + +\`\`\`markdown +--- +slug: [generated-slug] +title: "${topic}" +description: "[1-2 sentence description for SEO]" +date: [today's date YYYY-MM-DD] +status: draft +coverImage: null +tags: + - [tag1] + - [tag2] +--- + +[Article content following the structure above] +\`\`\` + +## Step 5: Save the Article + +Write to: \`${projectPath}/blog/articles/{slug}.md\` + +## Step 6: Review with User + +Show the article to the user and ask: +"Here's the draft. Would you like me to: +1. Edit any sections? +2. Adjust the tone? +3. Generate a cover image? +4. Publish it (change status to 'published')?" + +## Checklist (from tone guide) +- [ ] Opening hook stops the scroll +- [ ] At least one vulnerable moment (if appropriate) +- [ ] Short paragraphs (max 3 sentences) +- [ ] Connects to action, not just contemplation +- [ ] Ends with invitation or grounded statement`, + }, + }, + ], + }; + }, + }); + +/** + * EDIT_ARTICLE - Workflow for editing an existing article + */ +export const createEditArticlePrompt = (_env: Env) => + createPrompt({ + name: "EDIT_ARTICLE", + title: "Edit Existing Article", + description: `Workflow for editing an existing blog article. + +Provides guidance on: +- Reading the current article +- Piecemeal editing techniques +- Maintaining consistent voice`, + argsSchema: { + slug: z.string().describe("Article slug (filename without .md)"), + editRequest: z + .string() + .optional() + .describe("Specific edit request from user"), + projectPath: z + .string() + .optional() + .describe("Project root path (default: current directory)"), + }, + execute: ({ args }): GetPromptResult => { + const projectPath = args.projectPath || "."; + const { slug, editRequest } = args; + + return { + description: `Edit article: ${slug}`, + messages: [ + { + role: "user", + content: { + type: "text", + text: `# Edit Article: ${slug} + +## Step 1: Read Current Article + +Read: \`${projectPath}/blog/articles/${slug}.md\` + +## Step 2: Read Tone of Voice Guide + +To maintain consistency, also read: +\`${projectPath}/blog/tone-of-voice.md\` + +${ + editRequest + ? `## Edit Request + +The user wants: +${editRequest} +` + : "" +} + +## Editing Techniques + +### For Small Changes +Use precise text replacement: +- Find the exact text to change +- Replace with new text +- Preserve surrounding context + +### For Structural Changes +- Maintain the Hook → Develop → Turn → Close structure +- Keep paragraph rhythm consistent +- Preserve the author's voice + +### For Tone Adjustments +Reference the tone guide for: +- Vocabulary preferences +- Sentence rhythm +- Emotional register + +## Step 3: Make Edits + +Edit the file at: \`${projectPath}/blog/articles/${slug}.md\` + +Update the frontmatter if needed: +- Update \`date\` if content changes significantly +- Keep \`slug\` unchanged (breaks URLs) + +## Step 4: Show Changes + +Tell the user what was changed: +"I made the following edits to '${slug}': +- [Change 1] +- [Change 2] + +Would you like any additional changes?"`, + }, + }, + ], + }; + }, + }); + +/** + * All prompt factory functions. + */ +export const prompts = [ + createSetupProjectPrompt, + createToneOfVoicePrompt, + createVisualStylePrompt, + createWriteArticlePrompt, + createEditArticlePrompt, +]; diff --git a/blog/server/resources/index.ts b/blog/server/resources/index.ts new file mode 100644 index 00000000..fdf59ad9 --- /dev/null +++ b/blog/server/resources/index.ts @@ -0,0 +1,371 @@ +/** + * Blog MCP Resources + * + * Provides templates for creating tone of voice and visual style guides. + * These are read-only templates that can be used as starting points. + */ + +import { createPublicResource } from "@decocms/runtime"; +import type { Env } from "../types/env.ts"; + +/** + * Tone of Voice Template + * + * A comprehensive template for creating a tone of voice guide. + * Based on the forensic analysis pattern from vibegui.com. + */ +const TONE_OF_VOICE_TEMPLATE = `# [Author Name]'s Tone of Voice: A Writing Guide + +--- + +## CONTEXT + +This document defines your unique writing voice. Use it as a reference when writing articles to maintain consistency across all content. + +**Target audience**: AI agents, ghostwriters, or yourself when seeking consistency. + +--- + +## SECTION 1: CORE IDENTITY + +### 1.1 The Essence + +[Describe how you write - are you conversational, formal, provocative, nurturing?] + +**Primary archetype**: [e.g., "The Intellectual Warrior", "The Wise Friend", "The Skeptical Optimist"] + +**Voice DNA**: +- [Trait 1: e.g., "Confessional without being self-indulgent"] +- [Trait 2: e.g., "Philosophical without being abstract"] +- [Trait 3: e.g., "Intense without being aggressive"] + +### 1.2 The Person Behind the Words + +[Brief bio that explains your perspective and why people should listen to you] + +**Core tensions that create your unique voice**: +- [Tension 1: e.g., "Optimistic about the future / Honest about difficulties"] +- [Tension 2: e.g., "Deeply philosophical / Obsessively pragmatic"] + +--- + +## SECTION 2: HOOK ARCHITECTURE + +### Opening Patterns + +Every first sentence should stop the scroll. Use these patterns: + +#### Pattern 1: The Confession Hook +Start with radical vulnerability or counterintuitive admission. + +> Example: "I spent 30 years desperately seeking recognition from others." + +**Template**: "I [surprising admission about self]" + +#### Pattern 2: The Philosophical Provocation +Challenge a widely-held assumption in the first line. + +> Example: "The pessimist has the easiest job: they just need to wait for things to go wrong." + +**Template**: "The [common thing] is actually [unexpected reframe]" + +#### Pattern 3: The Story Seed +Begin with a concrete, specific moment. + +> Example: "Yesterday I had the privilege of interviewing [Person]." + +**Template**: "[Time marker] I [specific action] that [revealed insight]" + +### Anti-Patterns (What NOT to do) +- ❌ "5 Tips for..." or "How to..." openings +- ❌ Questions that feel rhetorical or manipulative +- ❌ "In today's world..." (cliché setup) + +--- + +## SECTION 3: STRUCTURAL BLUEPRINTS + +### Short-Form (150-400 words) + +\`\`\` +HOOK (1-2 sentences) +├── Confession/Provocation/Declaration +└── Creates immediate tension or curiosity + +DEVELOPMENT (3-5 paragraphs) +├── Short paragraphs (often single sentences) +├── Builds argument through examples or story +└── Includes at least one moment of vulnerability + +TURN/INSIGHT (1-2 sentences) +└── The "aha" that reframes everything + +INVITATION/CLOSE (1-2 sentences) +├── Direct challenge to reader OR +├── Quote from philosophical influence OR +└── Simple, grounded statement +\`\`\` + +### Long-Form (800-2000 words) + +\`\`\` +OPENING ARC (10-15%) +├── Personal hook with immediate specificity +└── Sets emotional stakes + +HONEST ASSESSMENT (25-30%) +├── What actually happened +├── What didn't work +└── No sugar-coating + +LESSONS EXTRACTED (30-35%) +├── Section headers (brief, punchy) +└── Each lesson connects philosophy to practice + +FORWARD LOOK (10-15%) +├── What this means for the future +└── Commitment statement +\`\`\` + +--- + +## SECTION 4: VOCABULARY + +### Words to Use Often +- [Word 1] - [when/why to use] +- [Word 2] - [when/why to use] +- [Word 3] - [when/why to use] + +### Words to Avoid +- [Word 1] - [why to avoid, e.g., "corporate jargon"] +- [Word 2] - [why to avoid, e.g., "hedging language"] + +--- + +## SECTION 5: EMOTIONAL REGISTER + +Your emotional register sits at "[contained intensity / warm invitation / etc.]" + +| Too Cold | Your Zone | Too Hot | +|----------|-----------|---------| +| "[cold example]" | "[your style example]" | "[overheated example]" | + +--- + +## SECTION 6: CLOSE PATTERNS + +### How to End Articles + +**Type 1: The Direct Challenge** +> "You decide which result you want." + +**Type 2: The Quiet Invitation** +> "My invitation for you: [specific action]." + +**Type 3: The Philosophical Quote Close** +> "[Quote]" — [Author] + +**Type 4: The Grounded Statement** +> "That feels like a good place to start." + +--- + +## QUICK REFERENCE CHECKLIST + +- [ ] Opening hook stops the scroll +- [ ] First-person voice throughout +- [ ] At least one vulnerable moment that's specific +- [ ] Short paragraphs (max 3 sentences) +- [ ] Active verbs ("I decided," not "It was decided") +- [ ] Ends with invitation or grounded statement +- [ ] No hedging language + +--- + +*This guide should be updated as your voice evolves.* +`; + +/** + * Visual Style Template + * + * A template for creating a visual style guide for cover images. + */ +const VISUAL_STYLE_TEMPLATE = `# Visual Style Guide + +## Overview + +This document defines the visual language for all imagery. Use this when generating cover images for articles. + +--- + +## Color Palette + +| Role | Hex | Description | +|------|-----|-------------| +| **Background** | #XXXXXX | [Description - e.g., "Deep forest green - dark, grounding"] | +| **Accent** | #XXXXXX | [Description - e.g., "Bright lime - energetic, alive"] | +| **Mid-tones** | Interpolate between the two | For gradients and transitions | + +**Rule**: [e.g., "Monochromatic palette ONLY. No other colors."] + +--- + +## Aesthetic Style + +**Core Aesthetic**: [e.g., "Retro Comic Hero meets Digital Noir"] + +**Vibe**: [e.g., "Bold, heroic, retro-futuristic, gritty"] + +### Style Influences +- [Influence 1: e.g., "1950s-60s Comic Books: Bold compositions, dramatic angles"] +- [Influence 2: e.g., "Atomic Age Sci-Fi: Ray guns, cosmic themes"] +- [Influence 3: e.g., "Digital Art: Glitch effects, pixelation"] + +--- + +## Visual Effects (Required) + +- **[Effect 1]**: [e.g., "Heavy dithering patterns throughout"] +- **[Effect 2]**: [e.g., "Halftone dots like vintage printing"] +- **[Effect 3]**: [e.g., "Film grain texture overlay"] + +### Composition Style +- [e.g., "Bold, dramatic compositions"] +- [e.g., "Strong contrast between dark and bright areas"] +- [e.g., "Hero shots - subjects presented powerfully"] + +--- + +## Image Generation Prompt Template + +Use this as a base prompt: + +\`\`\` +Create a digital artwork with [background color description]. + +[DESCRIBE THE MAIN SUBJECT OR CONCEPT HERE] + +Style influences: [list your style influences]. + +Apply effects: [list required effects]. + +Bold dramatic composition with strong contrast. [Accent color] for highlights and glowing elements. + +[Color palette rule]. + +[Overall vibe description]. + +[Dimensions: e.g., "1200x630 landscape"] +\`\`\` + +--- + +## Example Prompts + +### Article Header (1200x630) +\`\`\` +Create a landscape digital artwork (1200x630) with [background]. + +[CONCEPT] depicted in [style]. Bold, heroic composition. + +[Effects]. [Accent color] for highlights. + +[Color rule]. No text. + +Style: [style description]. +\`\`\` + +### Square Social Post (1080x1080) +\`\`\` +Create a square digital artwork (1080x1080) with [background]. + +[CONCEPT] in [style]. Dynamic pose or dramatic composition. + +[Effects]. + +[Accent color] accents against [background]. + +[Color rule]. [Vibe]. +\`\`\` + +--- + +## What to Avoid + +- ❌ [e.g., "Other colors outside the palette"] +- ❌ [e.g., "Clean, smooth gradients (use effects instead)"] +- ❌ [e.g., "Photorealistic imagery"] +- ❌ [e.g., "Text overlays (add text separately)"] +- ❌ [e.g., "Generic stock photo aesthetics"] + +--- + +## Conceptual Adaptations + +When representing different concepts, adapt the style: + +| Concept | Visual Treatment | +|---------|-----------------| +| **Technology** | [e.g., "Robot heroes, circuit patterns"] | +| **Leadership** | [e.g., "Heroic silhouette, upward angle"] | +| **Growth** | [e.g., "Ascending figure, explosive energy"] | +| **Philosophy** | [e.g., "Contemplative pose, cosmic background"] | +| **Writing** | [e.g., "Typewriter keys, speech bubbles"] | + +--- + +## Technical Specs + +| Use Case | Dimensions | Format | +|----------|------------|--------| +| Article Header / OG Image | 1200x630 | PNG | +| Square Social Post | 1080x1080 | PNG | +| Story/Vertical | 1080x1920 | PNG | +| Favicon/Icon | 512x512 | PNG | + +--- + +*Update this guide as your visual identity evolves.* +`; + +/** + * Create tone of voice template resource + */ +export const createToneOfVoiceTemplateResource = (_env: Env) => + createPublicResource({ + uri: "resource://tone-of-voice-template", + name: "Tone of Voice Template", + description: + "A comprehensive template for creating a tone of voice writing guide. Use this as a starting point when running TONE_OF_VOICE_TEMPLATE prompt.", + mimeType: "text/markdown", + read: () => ({ + uri: "resource://tone-of-voice-template", + mimeType: "text/markdown", + text: TONE_OF_VOICE_TEMPLATE, + }), + }); + +/** + * Create visual style template resource + */ +export const createVisualStyleTemplateResource = (_env: Env) => + createPublicResource({ + uri: "resource://visual-style-template", + name: "Visual Style Template", + description: + "A template for creating a visual style guide for cover images. Use this as a starting point when running VISUAL_STYLE_TEMPLATE prompt.", + mimeType: "text/markdown", + read: () => ({ + uri: "resource://visual-style-template", + mimeType: "text/markdown", + text: VISUAL_STYLE_TEMPLATE, + }), + }); + +/** + * All resource factory functions. + */ +export const resources = [ + createToneOfVoiceTemplateResource, + createVisualStyleTemplateResource, +]; diff --git a/blog/server/serve.ts b/blog/server/serve.ts new file mode 100644 index 00000000..17c4c204 --- /dev/null +++ b/blog/server/serve.ts @@ -0,0 +1,186 @@ +#!/usr/bin/env bun +/** + * Blog MCP - Serve & Link + * + * Starts the blog MCP and exposes it via deco link with a ready-to-add URL. + * + * Usage: + * bun run serve # Start and expose via tunnel + * bun run serve --port 8080 # Custom port + */ + +import { spawn } from "node:child_process"; +import { platform } from "node:os"; +import { resolve } from "node:path"; + +const DEFAULT_PORT = 8010; + +/** + * Parse CLI arguments + */ +function parseArgs(): { port: number } { + const args = process.argv.slice(2); + let port = parseInt(process.env.PORT || String(DEFAULT_PORT), 10); + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === "serve") continue; + + if (arg === "--port" || arg === "-p") { + const p = parseInt(args[++i], 10); + if (!isNaN(p)) port = p; + continue; + } + } + + return { port }; +} + +/** + * Copy text to clipboard + */ +function copyToClipboard(text: string): Promise { + return new Promise((resolvePromise) => { + const os = platform(); + let cmd: string; + let args: string[]; + + if (os === "darwin") { + cmd = "pbcopy"; + args = []; + } else if (os === "win32") { + cmd = "clip"; + args = []; + } else { + cmd = "xclip"; + args = ["-selection", "clipboard"]; + } + + try { + const proc = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] }); + proc.stdin?.write(text); + proc.stdin?.end(); + proc.on("close", (code) => resolvePromise(code === 0)); + proc.on("error", () => resolvePromise(false)); + } catch { + resolvePromise(false); + } + }); +} + +const { port } = parseArgs(); + +let publicUrl = ""; + +/** + * Show the MCP URL banner when we detect the tunnel URL + */ +async function showMcpUrl(tunnelUrl: string) { + if (publicUrl) return; + publicUrl = tunnelUrl; + + const mcpUrl = `${publicUrl}/mcp`; + + const copied = await copyToClipboard(mcpUrl); + + console.log(` + +╔═══════════════════════════════════════════════════════════════════════╗ +║ ✅ Blog MCP Ready! ║ +╠═══════════════════════════════════════════════════════════════════════╣ +║ ║ +║ Add this MCP URL to your Deco Mesh: ║ +║ ║ +║ ${mcpUrl.padEnd(67)}║ +║ ║ +║ ${copied ? "📋 Copied to clipboard!" : "Copy the URL above"} ║ +║ ║ +║ Steps: ║ +║ 1. Open mesh-admin.decocms.com ║ +║ 2. Go to Connections → Add Custom MCP ║ +║ 3. Paste the URL above ║ +║ ║ +╚═══════════════════════════════════════════════════════════════════════╝ +`); +} + +/** + * Check output for tunnel URL + */ +function checkForTunnelUrl(output: string) { + const urlMatch = output.match(/https:\/\/[^\s()"']+\.deco\.(site|host)/); + if (urlMatch) { + const url = urlMatch[0].replace(/[()]/g, ""); + showMcpUrl(url); + } +} + +console.log(` +╔═══════════════════════════════════════════════════════════════════════╗ +║ Blog MCP - Serve & Link ║ +╠═══════════════════════════════════════════════════════════════════════╣ +║ 🔌 Port: ${port.toString().padEnd(61)}║ +║ ║ +║ ⚠️ Note: Only ONE deco link tunnel can run at a time per machine. ║ +║ Stop other 'serve' commands before starting this one. ║ +╚═══════════════════════════════════════════════════════════════════════╝ + +Starting server and tunnel... +`); + +// Get the directory of this script to find main.ts +const scriptDir = import.meta.dirname || resolve(process.cwd(), "server"); +const mainScript = resolve(scriptDir, "main.ts"); + +// Start the MCP server in background +const serverProcess = spawn("bun", ["run", "--hot", mainScript], { + stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, PORT: port.toString() }, +}); + +serverProcess.stdout?.on("data", (data) => { + process.stdout.write(data); +}); + +serverProcess.stderr?.on("data", (data) => { + process.stderr.write(data); +}); + +// Wait for server to start +await new Promise((r) => setTimeout(r, 2000)); + +// Run deco link to get the public URL +const decoLink = spawn("deco", ["link", "-p", port.toString()], { + stdio: ["inherit", "pipe", "pipe"], +}); + +decoLink.stdout?.on("data", (data) => { + const output = data.toString(); + process.stdout.write(output); + checkForTunnelUrl(output); +}); + +decoLink.stderr?.on("data", (data) => { + const output = data.toString(); + process.stderr.write(output); + checkForTunnelUrl(output); +}); + +// Handle exit +process.on("SIGINT", () => { + serverProcess.kill(); + decoLink.kill(); + process.exit(0); +}); + +process.on("SIGTERM", () => { + serverProcess.kill(); + decoLink.kill(); + process.exit(0); +}); + +decoLink.on("close", (code) => { + serverProcess.kill(); + process.exit(code || 0); +}); diff --git a/blog/server/tools/filesystem.ts b/blog/server/tools/filesystem.ts new file mode 100644 index 00000000..eb0967c2 --- /dev/null +++ b/blog/server/tools/filesystem.ts @@ -0,0 +1,259 @@ +/** + * Filesystem Tools for Blog MCP + * + * Uses OBJECT_STORAGE binding to read/write blog files directly. + * When OBJECT_STORAGE is connected, the blog MCP becomes fully self-contained. + */ + +import { createTool } from "@decocms/runtime/tools"; +import { z } from "zod"; + +// Use any for now to bypass complex type inference issues +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Env = any; + +/** + * BLOG_READ_STYLE_GUIDE - Read the tone of voice or visual style guide + */ +export const createReadStyleGuideTool = (env: Env) => + createTool({ + id: "BLOG_READ_STYLE_GUIDE", + description: `Read the tone of voice or visual style guide from the blog folder. + +Requires OBJECT_STORAGE binding connected to a folder with a blog/ subfolder.`, + inputSchema: z.object({ + guide: z + .enum(["tone-of-voice", "visual-style"]) + .describe("Which guide to read"), + }), + outputSchema: z.object({ + success: z.boolean(), + content: z.string().optional(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const storage = env.bindings?.OBJECT_STORAGE; + + if (!storage) { + return { + success: false, + error: + "OBJECT_STORAGE binding not configured. Connect a local-fs MCP pointing to a folder with a blog/ subfolder.", + }; + } + + try { + const path = `blog/${context.guide}.md`; + // Use read_file which is available on local-fs + const result = await (storage as any).call("read_text_file", { path }); + return { + success: true, + content: result.content || result, + }; + } catch (error) { + return { + success: false, + error: `Failed to read ${context.guide}: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BLOG_LIST_ARTICLES - List all articles in the blog/articles folder + */ +export const createListArticlesTool = (env: Env) => + createTool({ + id: "BLOG_LIST_ARTICLES", + description: `List all article files in the blog/articles folder. + +Requires OBJECT_STORAGE binding.`, + inputSchema: z.object({}), + outputSchema: z.object({ + success: z.boolean(), + articles: z.array(z.string()).optional(), + count: z.number().optional(), + error: z.string().optional(), + }), + execute: async () => { + const storage = env.bindings?.OBJECT_STORAGE; + + if (!storage) { + return { + success: false, + error: "OBJECT_STORAGE binding not configured.", + }; + } + + try { + const result = await storage.call("LIST_OBJECTS", { + prefix: "blog/articles/", + delimiter: "/", + }); + + const files = (result.objects || []) + .filter((obj: { key: string }) => obj.key.endsWith(".md")) + .map((obj: { key: string }) => { + const filename = obj.key.split("/").pop() || ""; + return filename.replace(".md", ""); + }); + + return { + success: true, + articles: files, + count: files.length, + }; + } catch (error) { + return { + success: false, + error: `Failed to list articles: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BLOG_READ_ARTICLE - Read an article's content + */ +export const createReadArticleTool = (env: Env) => + createTool({ + id: "BLOG_READ_ARTICLE", + description: `Read an article from blog/articles/{slug}.md. + +Requires OBJECT_STORAGE binding.`, + inputSchema: z.object({ + slug: z.string().describe("Article slug (filename without .md)"), + }), + outputSchema: z.object({ + success: z.boolean(), + content: z.string().optional(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const storage = env.bindings?.OBJECT_STORAGE; + + if (!storage) { + return { + success: false, + error: "OBJECT_STORAGE binding not configured.", + }; + } + + try { + const path = `blog/articles/${context.slug}.md`; + // Use read_text_file which is available on local-fs + const result = await (storage as any).call("read_text_file", { path }); + return { + success: true, + content: result.content || result, + }; + } catch (error) { + return { + success: false, + error: `Failed to read article: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BLOG_WRITE_ARTICLE - Write an article to blog/articles/{slug}.md + */ +export const createWriteArticleTool = (env: Env) => + createTool({ + id: "BLOG_WRITE_ARTICLE", + description: `Write an article to blog/articles/{slug}.md. + +The content should include YAML frontmatter. Use ARTICLE_FRONTMATTER to generate it. + +Requires OBJECT_STORAGE binding.`, + inputSchema: z.object({ + slug: z.string().describe("Article slug (becomes filename)"), + content: z + .string() + .describe("Full article content including YAML frontmatter"), + }), + outputSchema: z.object({ + success: z.boolean(), + path: z.string().optional(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const storage = env.bindings?.OBJECT_STORAGE; + + if (!storage) { + return { + success: false, + error: "OBJECT_STORAGE binding not configured.", + }; + } + + try { + const path = `blog/articles/${context.slug}.md`; + // Use write_file which is available on local-fs + await (storage as any).call("write_file", { + path, + content: context.content, + }); + return { + success: true, + path, + }; + } catch (error) { + return { + success: false, + error: `Failed to write article: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BLOG_DELETE_ARTICLE - Delete an article + */ +export const createDeleteArticleTool = (env: Env) => + createTool({ + id: "BLOG_DELETE_ARTICLE", + description: `Delete an article from blog/articles/{slug}.md. + +Requires OBJECT_STORAGE binding.`, + inputSchema: z.object({ + slug: z.string().describe("Article slug to delete"), + }), + outputSchema: z.object({ + success: z.boolean(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const storage = env.bindings?.OBJECT_STORAGE; + + if (!storage) { + return { + success: false, + error: "OBJECT_STORAGE binding not configured.", + }; + } + + try { + const path = `blog/articles/${context.slug}.md`; + await storage.call("DELETE_OBJECT", { key: path }); + return { success: true }; + } catch (error) { + return { + success: false, + error: `Failed to delete article: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * All filesystem tool factories + */ +export const filesystemTools = [ + createReadStyleGuideTool, + createListArticlesTool, + createReadArticleTool, + createWriteArticleTool, + createDeleteArticleTool, +]; diff --git a/blog/server/tools/index.ts b/blog/server/tools/index.ts new file mode 100644 index 00000000..17c7c860 --- /dev/null +++ b/blog/server/tools/index.ts @@ -0,0 +1,273 @@ +/** + * Blog MCP Tools + * + * Tools for blog article management: + * - COVER_IMAGE_GENERATE - Generate cover images using IMAGE_GENERATOR binding + * - ARTICLE_FRONTMATTER - Generate valid frontmatter for a new article + * - ARTICLE_VALIDATE - Validate article markdown structure and frontmatter + * + * Filesystem tools (require OBJECT_STORAGE binding): + * - BLOG_READ_STYLE_GUIDE - Read tone-of-voice.md or visual-style.md + * - BLOG_LIST_ARTICLES - List all articles + * - BLOG_READ_ARTICLE - Read an article + * - BLOG_WRITE_ARTICLE - Write an article + * - BLOG_DELETE_ARTICLE - Delete an article + */ + +import { createTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import { filesystemTools } from "./filesystem.ts"; + +// Use any for now to bypass complex type inference issues +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Env = any; + +/** + * Generate today's date in YYYY-MM-DD format + */ +function getTodayDate(): string { + return new Date().toISOString().split("T")[0]; +} + +/** + * Generate a URL-friendly slug from a title + */ +function generateSlug(title: string): string { + return title + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, "") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); +} + +/** + * COVER_IMAGE_GENERATE - Generate cover image following visual style guide + * + * IMPORTANT: Before calling this tool, read blog/visual-style.md to get the + * image generation style guidelines. Use those guidelines to construct the prompt. + */ +export const createCoverImageTool = (env: Env) => + createTool({ + id: "COVER_IMAGE_GENERATE", + description: `Generate a cover image for an article. + +IMPORTANT: Before calling this tool, read \`blog/visual-style.md\` to understand the visual style guidelines. The prompt should follow those guidelines. + +The tool uses the IMAGE_GENERATOR binding (nanobanana) to generate the image.`, + inputSchema: z.object({ + prompt: z + .string() + .describe( + "Image generation prompt following the visual style guide from blog/visual-style.md", + ), + articleSlug: z + .string() + .describe("Article slug - used to name the image file"), + width: z.number().optional().describe("Image width (default 1200)"), + height: z.number().optional().describe("Image height (default 630)"), + }), + outputSchema: z.object({ + success: z.boolean(), + imageUrl: z.string().optional(), + suggestedPath: z.string().optional(), + instructions: z.string().optional(), + error: z.string().optional(), + suggestion: z.string().optional(), + }), + execute: async ({ context }) => { + const { prompt, articleSlug, width = 1200, height = 630 } = context; + const imageGenerator = env.bindings?.IMAGE_GENERATOR; + + if (!imageGenerator) { + return { + success: false, + error: + "IMAGE_GENERATOR binding not configured. Connect the nanobanana MCP to enable image generation.", + suggestion: + "You can manually create a cover image and set the coverImage path in the article frontmatter.", + }; + } + + try { + // Call the image generator + const result = await imageGenerator.call("IMAGE_GENERATE", { + prompt, + width, + height, + }); + + return { + success: true, + imageUrl: result.url, + suggestedPath: `/images/articles/${articleSlug}.png`, + instructions: + "Download the image and save it to the suggested path, then update the article frontmatter with the coverImage path.", + }; + } catch (error) { + return { + success: false, + error: `Image generation failed: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * ARTICLE_FRONTMATTER - Generate valid frontmatter for a new article + */ +export const createArticleFrontmatterTool = (_env: Env) => + createTool({ + id: "ARTICLE_FRONTMATTER", + description: + "Generate valid YAML frontmatter for a new article. Returns the frontmatter block ready to use.", + inputSchema: z.object({ + title: z.string().describe("Article title"), + description: z + .string() + .describe("Short description for SEO (1-2 sentences)"), + tags: z.array(z.string()).optional().describe("Article tags"), + status: z + .enum(["draft", "published"]) + .optional() + .describe("Article status (default: draft)"), + }), + outputSchema: z.object({ + frontmatter: z.string().describe("YAML frontmatter block"), + slug: z.string().describe("Generated slug"), + filePath: z.string().describe("Suggested file path"), + }), + execute: async ({ context }) => { + const { title, description, tags = [], status = "draft" } = context; + const slug = generateSlug(title); + const date = getTodayDate(); + + const frontmatter = `--- +slug: ${slug} +title: "${title.replace(/"/g, '\\"')}" +description: "${description.replace(/"/g, '\\"')}" +date: ${date} +status: ${status} +coverImage: null +tags: +${tags.map((tag) => ` - ${tag}`).join("\n") || " []"} +---`; + + return { + frontmatter, + slug, + filePath: `blog/articles/${slug}.md`, + }; + }, + }); + +/** + * ARTICLE_VALIDATE - Validate article markdown structure and frontmatter + */ +export const createArticleValidateTool = (_env: Env) => + createTool({ + id: "ARTICLE_VALIDATE", + description: + "Validate an article's markdown structure and frontmatter. Returns validation results and suggestions.", + inputSchema: z.object({ + content: z + .string() + .describe("Full article content including frontmatter"), + }), + outputSchema: z.object({ + valid: z.boolean(), + issues: z.array(z.string()), + suggestions: z.array(z.string()), + stats: z + .object({ + wordCount: z.number(), + paragraphCount: z.number(), + hasHeadings: z.boolean(), + }) + .optional(), + }), + execute: async ({ context }) => { + const { content } = context; + const issues: string[] = []; + const suggestions: string[] = []; + + // Check for frontmatter + if (!content.startsWith("---")) { + issues.push("Missing frontmatter block at the start of the file"); + return { + valid: false, + issues, + suggestions: ["Add YAML frontmatter starting with ---"], + }; + } + + // Extract frontmatter + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!frontmatterMatch) { + issues.push("Frontmatter block not properly closed with ---"); + return { + valid: false, + issues, + suggestions: ["Ensure frontmatter ends with ---"], + }; + } + + const frontmatter = frontmatterMatch[1]; + const body = content.slice(frontmatterMatch[0].length).trim(); + + // Check required frontmatter fields + const requiredFields = ["slug", "title", "description", "date", "status"]; + for (const field of requiredFields) { + if (!frontmatter.includes(`${field}:`)) { + issues.push(`Missing required frontmatter field: ${field}`); + } + } + + // Check status value + const statusMatch = frontmatter.match(/status:\s*(draft|published)/); + if (!statusMatch) { + issues.push("Status must be 'draft' or 'published'"); + } + + // Check date format + const dateMatch = frontmatter.match(/date:\s*(\d{4}-\d{2}-\d{2})/); + if (!dateMatch) { + issues.push("Date should be in YYYY-MM-DD format"); + } + + // Check body content + if (body.length < 100) { + suggestions.push( + "Article body is quite short. Consider adding more content.", + ); + } + + // Check for headings + if (!body.includes("#")) { + suggestions.push( + "Consider adding section headings for better structure.", + ); + } + + return { + valid: issues.length === 0, + issues, + suggestions, + stats: { + wordCount: body.split(/\s+/).length, + paragraphCount: body.split(/\n\n+/).length, + hasHeadings: body.includes("#"), + }, + }; + }, + }); + +/** + * All tool factory functions. + */ +export const tools = [ + createCoverImageTool, + createArticleFrontmatterTool, + createArticleValidateTool, + ...filesystemTools, +]; diff --git a/blog/server/types/env.ts b/blog/server/types/env.ts new file mode 100644 index 00000000..b63a5d76 --- /dev/null +++ b/blog/server/types/env.ts @@ -0,0 +1,22 @@ +/** + * Environment Type Definitions for Blog MCP + */ +import { + BindingOf, + type DefaultEnv, + type BindingRegistry, +} from "@decocms/runtime"; +import { z } from "zod"; + +export const StateSchema = z.object({ + OBJECT_STORAGE: BindingOf("@deco/object-storage") + .optional() + .describe( + "Object storage binding - select a folder with a blog/ subfolder containing articles/, tone-of-voice.md, and visual-style.md", + ), + IMAGE_GENERATOR: BindingOf("@deco/nanobanana") + .optional() + .describe("Image generation binding for cover images"), +}); + +export type Env = DefaultEnv; diff --git a/blog/tsconfig.json b/blog/tsconfig.json new file mode 100644 index 00000000..5f2d051f --- /dev/null +++ b/blog/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "verbatimModuleSyntax": false, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "allowJs": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path Aliases */ + "baseUrl": ".", + "paths": { + "server/*": ["./server/*"] + } + }, + "include": [ + "server" + ] +} diff --git a/bookmarks/README.md b/bookmarks/README.md new file mode 100644 index 00000000..869f1fb3 --- /dev/null +++ b/bookmarks/README.md @@ -0,0 +1,105 @@ +# Bookmarks MCP + +Bookmark management with AI enrichment via Perplexity and Firecrawl. + +## Overview + +The Bookmarks MCP provides tools for managing bookmarks stored in Supabase with AI-powered enrichment capabilities. + +## Features + +- **CRUD Operations** - Create, read, update, delete bookmarks +- **Full-Text Search** - Search across titles, descriptions, and content +- **AI Research** - Use Perplexity to research bookmark content +- **Web Scraping** - Use Firecrawl to extract page content +- **Auto-Classification** - Generate tags and insights automatically +- **Browser Import** - Import from Chrome or Firefox exports + +## Bookmark Schema + +Each bookmark includes: + +| Field | Description | +|-------|-------------| +| `url` | Bookmark URL (unique) | +| `title` | Page title | +| `description` | Brief description | +| `stars` | Rating (0-5) | +| `tags` | Array of tags | +| `perplexity_research` | AI-generated research summary | +| `firecrawl_content` | Scraped page content | +| `insight_dev` | Insight for developers | +| `insight_founder` | Insight for founders | +| `insight_investor` | Insight for investors | +| `reading_time_min` | Estimated reading time | +| `language` | Content language | + +## Tools + +### CRUD + +| Tool | Description | +|------|-------------| +| `BOOKMARK_LIST` | List bookmarks with filters | +| `BOOKMARK_GET` | Get single bookmark by URL or ID | +| `BOOKMARK_CREATE` | Create new bookmark | +| `BOOKMARK_UPDATE` | Update bookmark | +| `BOOKMARK_DELETE` | Delete bookmark | +| `BOOKMARK_SEARCH` | Full-text search | + +### Enrichment + +| Tool | Description | +|------|-------------| +| `BOOKMARK_RESEARCH` | Research URL with Perplexity | +| `BOOKMARK_SCRAPE` | Scrape content with Firecrawl | +| `BOOKMARK_CLASSIFY` | Auto-classify with tags and insights | +| `BOOKMARK_ENRICH_BATCH` | Batch enrich multiple bookmarks | + +### Import + +| Tool | Description | +|------|-------------| +| `BOOKMARK_IMPORT_CHROME` | Import from Chrome HTML export | +| `BOOKMARK_IMPORT_FIREFOX` | Import from Firefox export | + +## Prompts + +| Prompt | Description | +|--------|-------------| +| `SETUP_TABLES` | SQL to create bookmarks tables in Supabase | +| `ENRICH_WORKFLOW` | Workflow for bulk enriching bookmarks | + +## Bindings + +| Binding | Required | Description | +|---------|----------|-------------| +| `SUPABASE` | Yes | Supabase for bookmark storage | +| `PERPLEXITY` | Optional | AI research capabilities | +| `FIRECRAWL` | Optional | Web scraping capabilities | + +## Quick Start + +1. **Setup database**: Run `SETUP_TABLES` prompt in Supabase +2. **Create bookmarks**: Use `BOOKMARK_CREATE` or import from browser +3. **Enrich**: Use `BOOKMARK_ENRICH_BATCH` to add AI research + +## Development + +```bash +# Install dependencies +bun install + +# Run locally +bun run dev + +# Type check +bun run check + +# Build for production +bun run build +``` + +## License + +MIT diff --git a/bookmarks/app.json b/bookmarks/app.json new file mode 100644 index 00000000..2cfded32 --- /dev/null +++ b/bookmarks/app.json @@ -0,0 +1,24 @@ +{ + "scopeName": "deco", + "name": "bookmarks", + "friendlyName": "Bookmarks", + "connection": { + "type": "HTTP", + "url": "https://sites-bookmarks.decocache.com/mcp" + }, + "description": "Bookmark management with AI enrichment via Perplexity and Firecrawl.", + "icon": "https://assets.decocache.com/decocms/bookmarks-icon.png", + "unlisted": false, + "bindings": { + "SUPABASE": "@supabase/supabase", + "PERPLEXITY": "@deco/perplexity", + "FIRECRAWL": "@deco/firecrawl" + }, + "metadata": { + "categories": ["Productivity", "Research"], + "official": false, + "tags": ["bookmarks", "research", "ai", "perplexity", "firecrawl", "supabase"], + "short_description": "Manage and enrich bookmarks with AI research.", + "mesh_description": "The Bookmarks MCP provides tools for managing bookmarks stored in Supabase. It includes CRUD operations, full-text search, and AI enrichment capabilities. Use Perplexity to research bookmark content and Firecrawl to scrape page content. Import bookmarks from Chrome or Firefox exports. Each bookmark can store research summaries, insights from different perspectives (developer, founder, investor), and automatic classification with tags." + } +} diff --git a/bookmarks/bun.lock b/bookmarks/bun.lock new file mode 100644 index 00000000..5ae32e8a --- /dev/null +++ b/bookmarks/bun.lock @@ -0,0 +1,573 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "@decocms/bookmarks", + "dependencies": { + "@decocms/bindings": "^1.0.9", + "@decocms/runtime": "1.2.0", + "zod": "^4.0.0", + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "1.25.1", + "deco-cli": "^0.28.0", + "typescript": "^5.7.2", + }, + }, + }, + "packages": { + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.5", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w=="], + + "@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260127.0", "", {}, "sha512-4M1HLcWViSdT/pAeDGEB5x5P3sqW7UIi34QrBRnxXbqjAY9if8vBU/lWRWnM+UqKzxWGB2LYjEVOzZrp0jZL+w=="], + + "@deco-cx/warp-node": ["@deco-cx/warp-node@0.3.16", "", { "dependencies": { "undici": "^6.21.0", "ws": "^8.18.0" } }, "sha512-8cak+6YDrfJiYAkRqLCcywXrDaCkfKjbq/zU0zYUc5DSTt5bOzrA7RifqCLAfAgtEBw0mDdcr4IRPqGz65RdbA=="], + + "@decocms/bindings": ["@decocms/bindings@1.1.3", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.2", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-G7GvtGhXa/LoPllCVaeGAnkM9Mz5bG/bmUEfdno9+flKTIWAm9HUqSACX48jRv25OiAir6KbL1JX/xF6uHSUtQ=="], + + "@decocms/runtime": ["@decocms/runtime@1.2.0", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-HkhKBrCGYPlKyYW+iZxl7hmSwvzeJbBkFYE1+/0y5Ok1rlf/pxHHBHfiFlVgs1pr/seyFjEsqi2LnK8bMnDOEg=="], + + "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@supabase/auth-js": ["@supabase/auth-js@2.70.0", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg=="], + + "@supabase/functions-js": ["@supabase/functions-js@2.4.4", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA=="], + + "@supabase/node-fetch": ["@supabase/node-fetch@2.6.15", "", { "dependencies": { "whatwg-url": "^5.0.0" } }, "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ=="], + + "@supabase/postgrest-js": ["@supabase/postgrest-js@1.19.4", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw=="], + + "@supabase/realtime-js": ["@supabase/realtime-js@2.11.10", "", { "dependencies": { "@supabase/node-fetch": "^2.6.13", "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "ws": "^8.18.2" } }, "sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA=="], + + "@supabase/ssr": ["@supabase/ssr@0.6.1", "", { "dependencies": { "cookie": "^1.0.1" }, "peerDependencies": { "@supabase/supabase-js": "^2.43.4" } }, "sha512-QtQgEMvaDzr77Mk3vZ3jWg2/y+D8tExYF7vcJT+wQ8ysuvOeGGjYbZlvj5bHYsj/SpC0bihcisnwPrM4Gp5G4g=="], + + "@supabase/storage-js": ["@supabase/storage-js@2.7.1", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA=="], + + "@supabase/supabase-js": ["@supabase/supabase-js@2.50.0", "", { "dependencies": { "@supabase/auth-js": "2.70.0", "@supabase/functions-js": "2.4.4", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.19.4", "@supabase/realtime-js": "2.11.10", "@supabase/storage-js": "2.7.1" } }, "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg=="], + + "@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], + + "@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], + + "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="], + + "@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], + + "@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="], + + "@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], + + "@types/phoenix": ["@types/phoenix@1.6.7", "", {}, "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deco-cli": ["deco-cli@0.28.6", "", { "dependencies": { "@deco-cx/warp-node": "0.3.16", "@modelcontextprotocol/sdk": "1.25.1", "@supabase/ssr": "0.6.1", "@supabase/supabase-js": "2.50.0", "chalk": "^5.3.0", "commander": "^12.0.0", "glob": "^10.3.10", "ignore": "^7.0.5", "inquirer": "^9.2.15", "inquirer-search-checkbox": "^1.0.0", "inquirer-search-list": "^1.2.6", "jose": "^6.0.11", "json-schema-to-typescript": "^15.0.4", "object-hash": "^3.0.0", "prettier": "^3.6.2", "semver": "^7.6.0", "smol-toml": "^1.3.4", "zod": "^3.25.76" }, "bin": { "deco": "dist/cli.js", "deconfig": "dist/deconfig.js" } }, "sha512-IwdfHoZfrLVGTVULBJ2NRjEkD9dZafJSf3qYsZeer7CR5owQ1XLnDAKIwd/c6iwLZB6+2zrMjL4RNWhF2SzZbw=="], + + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], + + "external-editor": ["external-editor@2.2.0", "", { "dependencies": { "chardet": "^0.4.0", "iconv-lite": "^0.4.17", "tmp": "^0.0.33" } }, "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "figures": ["figures@2.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "fuzzy": ["fuzzy@0.1.3", "", {}, "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "inquirer": ["inquirer@9.3.8", "", { "dependencies": { "@inquirer/external-editor": "^1.0.2", "@inquirer/figures": "^1.0.3", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "1.0.0", "ora": "^5.4.1", "run-async": "^3.0.0", "rxjs": "^7.8.1", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" } }, "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w=="], + + "inquirer-search-checkbox": ["inquirer-search-checkbox@1.0.0", "", { "dependencies": { "chalk": "^2.3.0", "figures": "^2.0.0", "fuzzy": "^0.1.3", "inquirer": "^3.3.0" } }, "sha512-KR6kfe0+h7Zgyrj6GCBVgS4ZmmBhsXofcJoQv6EXZWxK+bpJZV9kOb2AaQ2fbjnH91G0tZWQaS5WteWygzXcmA=="], + + "inquirer-search-list": ["inquirer-search-list@1.2.6", "", { "dependencies": { "chalk": "^2.3.0", "figures": "^2.0.0", "fuzzy": "^0.1.3", "inquirer": "^3.3.0" } }, "sha512-C4pKSW7FOYnkAloH8rB4FiM91H1v08QFZZJh6KRt//bMfdDBIhgdX8wjHvrVH2bu5oIo6wYqGpzSBxkeClPxew=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + + "isbot": ["isbot@5.1.34", "", {}, "sha512-aCMIBSKd/XPRYdiCQTLC8QHH4YT8B3JUADu+7COgYIZPvkeoMcUHMRjZLM9/7V8fCj+l7FSREc1lOPNjzogo/A=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + + "json-schema-to-typescript": ["json-schema-to-typescript@15.0.4", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.5.5", "@types/json-schema": "^7.0.15", "@types/lodash": "^4.17.7", "is-glob": "^4.0.3", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "minimist": "^1.2.8", "prettier": "^3.2.5", "tinyglobby": "^0.2.9" }, "bin": { "json2ts": "dist/src/cli.js" } }, "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "run-async": ["run-async@3.0.0", "", {}, "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q=="], + + "rx-lite": ["rx-lite@4.0.8", "", {}, "sha512-Cun9QucwK6MIrp3mry/Y7hqD1oFqTYLQ4pGxaHTjIdaFDWRGGLikqp6u8LcWJnzpoALg9hap+JGk8sFIUuEGNA=="], + + "rx-lite-aggregates": ["rx-lite-aggregates@4.0.8", "", { "dependencies": { "rx-lite": "*" } }, "sha512-3xPNZGW93oCjiO7PtKxRK6iOVYBWBvtf9QHDfU23Oc+dLIQmAV//UnyXV/yihv81VS/UqoQPk4NegS8EFi55Hg=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="], + + "seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "zod-from-json-schema": ["zod-from-json-schema@0.5.2", "", { "dependencies": { "zod": "^4.0.17" } }, "sha512-/dNaicfdhJTOuUd4RImbLUE2g5yrSzzDjI/S6C2vO2ecAGZzn9UcRVgtyLSnENSmAOBRiSpUdzDS6fDWX3Z35g=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + + "@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@supabase/ssr/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "deco-cli/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "external-editor/chardet": ["chardet@0.4.2", "", {}, "sha512-j/Toj7f1z98Hh2cYo2BVr85EpIRWqUi7rtRSGxh/cqUjqrnJe9l9UE7IUGd2vQ2p+kSHLkSzObQPZPLUC6TQwg=="], + + "external-editor/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "inquirer-search-checkbox/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "inquirer-search-checkbox/inquirer": ["inquirer@3.3.0", "", { "dependencies": { "ansi-escapes": "^3.0.0", "chalk": "^2.0.0", "cli-cursor": "^2.1.0", "cli-width": "^2.0.0", "external-editor": "^2.0.4", "figures": "^2.0.0", "lodash": "^4.3.0", "mute-stream": "0.0.7", "run-async": "^2.2.0", "rx-lite": "^4.0.8", "rx-lite-aggregates": "^4.0.8", "string-width": "^2.1.0", "strip-ansi": "^4.0.0", "through": "^2.3.6" } }, "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ=="], + + "inquirer-search-list/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], + + "inquirer-search-list/inquirer": ["inquirer@3.3.0", "", { "dependencies": { "ansi-escapes": "^3.0.0", "chalk": "^2.0.0", "cli-cursor": "^2.1.0", "cli-width": "^2.0.0", "external-editor": "^2.0.4", "figures": "^2.0.0", "lodash": "^4.3.0", "mute-stream": "0.0.7", "run-async": "^2.2.0", "rx-lite": "^4.0.8", "rx-lite-aggregates": "^4.0.8", "string-width": "^2.1.0", "strip-ansi": "^4.0.0", "through": "^2.3.6" } }, "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ=="], + + "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "inquirer-search-checkbox/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "inquirer-search-checkbox/inquirer/ansi-escapes": ["ansi-escapes@3.2.0", "", {}, "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ=="], + + "inquirer-search-checkbox/inquirer/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], + + "inquirer-search-checkbox/inquirer/cli-width": ["cli-width@2.2.1", "", {}, "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw=="], + + "inquirer-search-checkbox/inquirer/mute-stream": ["mute-stream@0.0.7", "", {}, "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ=="], + + "inquirer-search-checkbox/inquirer/run-async": ["run-async@2.4.1", "", {}, "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="], + + "inquirer-search-checkbox/inquirer/string-width": ["string-width@2.1.1", "", { "dependencies": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" } }, "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw=="], + + "inquirer-search-checkbox/inquirer/strip-ansi": ["strip-ansi@4.0.0", "", { "dependencies": { "ansi-regex": "^3.0.0" } }, "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow=="], + + "inquirer-search-list/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + + "inquirer-search-list/inquirer/ansi-escapes": ["ansi-escapes@3.2.0", "", {}, "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ=="], + + "inquirer-search-list/inquirer/cli-cursor": ["cli-cursor@2.1.0", "", { "dependencies": { "restore-cursor": "^2.0.0" } }, "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw=="], + + "inquirer-search-list/inquirer/cli-width": ["cli-width@2.2.1", "", {}, "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw=="], + + "inquirer-search-list/inquirer/mute-stream": ["mute-stream@0.0.7", "", {}, "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ=="], + + "inquirer-search-list/inquirer/run-async": ["run-async@2.4.1", "", {}, "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="], + + "inquirer-search-list/inquirer/string-width": ["string-width@2.1.1", "", { "dependencies": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" } }, "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw=="], + + "inquirer-search-list/inquirer/strip-ansi": ["strip-ansi@4.0.0", "", { "dependencies": { "ansi-regex": "^3.0.0" } }, "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow=="], + + "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "ora/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "inquirer-search-checkbox/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "inquirer-search-checkbox/inquirer/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], + + "inquirer-search-checkbox/inquirer/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], + + "inquirer-search-checkbox/inquirer/strip-ansi/ansi-regex": ["ansi-regex@3.0.1", "", {}, "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw=="], + + "inquirer-search-list/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "inquirer-search-list/inquirer/cli-cursor/restore-cursor": ["restore-cursor@2.0.0", "", { "dependencies": { "onetime": "^2.0.0", "signal-exit": "^3.0.2" } }, "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q=="], + + "inquirer-search-list/inquirer/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], + + "inquirer-search-list/inquirer/strip-ansi/ansi-regex": ["ansi-regex@3.0.1", "", {}, "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw=="], + + "log-symbols/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "ora/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "inquirer-search-checkbox/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "inquirer-search-checkbox/inquirer/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], + + "inquirer-search-checkbox/inquirer/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "inquirer-search-list/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "inquirer-search-list/inquirer/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], + + "inquirer-search-list/inquirer/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "inquirer-search-checkbox/inquirer/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], + + "inquirer-search-list/inquirer/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="], + } +} diff --git a/bookmarks/package.json b/bookmarks/package.json new file mode 100644 index 00000000..810e84b8 --- /dev/null +++ b/bookmarks/package.json @@ -0,0 +1,32 @@ +{ + "name": "@decocms/bookmarks", + "version": "1.0.0", + "description": "Bookmark management with AI enrichment via Perplexity and Firecrawl", + "private": true, + "type": "module", + "scripts": { + "dev": "bun run --hot server/main.ts", + "serve": "bun run server/serve.ts", + "dev:link": "deco link -p 8011 -- PORT=8011 bun run dev", + "build:server": "NODE_ENV=production bun build server/main.ts --target=bun --outfile=dist/server/main.js", + "build": "bun run build:server", + "publish": "cat app.json | deco registry publish -w /shared/deco -y", + "check": "tsc --noEmit" + }, + "exports": { + "./tools": "./server/tools/index.ts" + }, + "dependencies": { + "@decocms/bindings": "^1.0.9", + "@decocms/runtime": "1.2.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "1.25.1", + "deco-cli": "^0.28.0", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/bookmarks/server/main.ts b/bookmarks/server/main.ts new file mode 100644 index 00000000..4bde5500 --- /dev/null +++ b/bookmarks/server/main.ts @@ -0,0 +1,133 @@ +/** + * Bookmarks MCP - Bookmark Management with AI Enrichment + * + * Manage bookmarks stored in Supabase with AI-powered enrichment. + * + * ## Features + * + * - **CRUD Operations** - Create, read, update, delete bookmarks + * - **Search** - Full-text search across bookmarks + * - **AI Research** - Use Perplexity to research bookmark content + * - **Web Scraping** - Use Firecrawl to extract page content + * - **Classification** - Auto-tag and categorize bookmarks + * - **Import** - Import from Chrome or Firefox exports + * + * ## Bookmark Schema + * + * Each bookmark includes: + * - Basic info: url, title, description, icon + * - Enrichment: perplexity_research, firecrawl_content + * - Insights: insight_dev, insight_founder, insight_investor + * - Metadata: tags, stars, reading_time_min, language + */ +import { withRuntime } from "@decocms/runtime"; +import { tools } from "./tools/index.ts"; +import { prompts } from "./prompts.ts"; +import { StateSchema, type Env } from "./types/env.ts"; +import type { BindingRegistry } from "@decocms/runtime"; + +export { StateSchema }; + +type Registry = BindingRegistry; + +const PORT = process.env.PORT || 8006; + +console.log("[bookmarks-mcp] Starting server..."); +console.log("[bookmarks-mcp] Port:", PORT); +console.log("[bookmarks-mcp] Tools count:", tools.length); +console.log("[bookmarks-mcp] Prompts count:", prompts.length); + +const runtime = withRuntime({ + configuration: { + scopes: ["SUPABASE::*", "PERPLEXITY::*", "FIRECRAWL::*"], + state: StateSchema, + }, + tools, + prompts, + resources: [], +}); + +console.log("[bookmarks-mcp] Runtime initialized"); + +/** + * Fetch handler with logging + */ +const fetchWithLogging = async (req: Request): Promise => { + const url = new URL(req.url); + const startTime = Date.now(); + + if (req.method === "POST" && url.pathname === "/mcp") { + try { + const body = await req.clone().json(); + const method = body?.method || "unknown"; + const toolName = body?.params?.name; + + if (method === "tools/call" && toolName) { + console.log(`[bookmarks-mcp] 🔧 Tool call: ${toolName}`); + } else if (method !== "unknown") { + console.log(`[bookmarks-mcp] 📨 Request: ${method}`); + } + } catch { + // Ignore JSON parse errors + } + } + + const response = await runtime.fetch(req); + + const duration = Date.now() - startTime; + if (duration > 100) { + console.log(`[bookmarks-mcp] ⏱️ Response in ${duration}ms`); + } + + return response; +}; + +// Start the server +Bun.serve({ + port: PORT, + hostname: "0.0.0.0", + idleTimeout: 0, + fetch: fetchWithLogging, + development: process.env.NODE_ENV !== "production", +}); + +console.log(""); +console.log("🔖 Bookmarks MCP running at: http://localhost:" + PORT + "/mcp"); +console.log(""); +console.log("[bookmarks-mcp] Available tools:"); +console.log(" CRUD:"); +console.log(" - BOOKMARK_LIST - List bookmarks with filters"); +console.log(" - BOOKMARK_GET - Get single bookmark"); +console.log(" - BOOKMARK_CREATE - Create new bookmark"); +console.log(" - BOOKMARK_UPDATE - Update bookmark"); +console.log(" - BOOKMARK_DELETE - Delete bookmark"); +console.log(" - BOOKMARK_SEARCH - Full-text search"); +console.log(""); +console.log(" Enrichment:"); +console.log(" - BOOKMARK_RESEARCH - Research with Perplexity"); +console.log(" - BOOKMARK_SCRAPE - Scrape with Firecrawl"); +console.log(" - BOOKMARK_CLASSIFY - Auto-classify with tags"); +console.log(" - BOOKMARK_ENRICH_BATCH - Batch enrich bookmarks"); +console.log(""); +console.log(" Import:"); +console.log(" - BOOKMARK_IMPORT_CHROME - Import Chrome bookmarks"); +console.log(" - BOOKMARK_IMPORT_FIREFOX - Import Firefox bookmarks"); +console.log(""); +console.log("[bookmarks-mcp] Required bindings:"); +console.log(" - SUPABASE (@supabase/supabase) - Bookmark storage"); +console.log(""); +console.log("[bookmarks-mcp] Optional bindings:"); +console.log(" - PERPLEXITY (@deco/perplexity) - AI research"); +console.log(" - FIRECRAWL (@deco/firecrawl) - Web scraping"); + +// Copy URL to clipboard on macOS +if (process.platform === "darwin") { + try { + const proc = Bun.spawn(["pbcopy"], { stdin: "pipe" }); + proc.stdin.write(`http://localhost:${PORT}/mcp`); + proc.stdin.end(); + console.log("[bookmarks-mcp] 📋 MCP URL copied to clipboard!"); + } catch { + // Ignore clipboard errors + } +} diff --git a/bookmarks/server/prompts.ts b/bookmarks/server/prompts.ts new file mode 100644 index 00000000..f7fdd3a2 --- /dev/null +++ b/bookmarks/server/prompts.ts @@ -0,0 +1,246 @@ +/** + * Bookmarks MCP Prompts + */ + +import { createPrompt, type GetPromptResult } from "@decocms/runtime"; +import { z } from "zod"; +import type { Env } from "./types/env.ts"; + +/** + * SETUP_TABLES - Instructions for creating bookmarks tables in Supabase + */ +export const createSetupTablesPrompt = (_env: Env) => + createPrompt({ + name: "SETUP_TABLES", + title: "Setup Bookmarks Tables", + description: `Create the required tables in Supabase for bookmark storage. + +Run this once when setting up a new project with bookmarks.`, + argsSchema: {}, + execute: (): GetPromptResult => { + return { + description: "Create bookmarks tables in Supabase", + messages: [ + { + role: "user", + content: { + type: "text", + text: `# Setup Bookmarks Tables + +## Required Tables + +Run these SQL statements in Supabase SQL Editor or via the SUPABASE binding: + +### 1. Bookmarks Table + +\`\`\`sql +CREATE TABLE IF NOT EXISTS bookmarks ( + id SERIAL PRIMARY KEY, + url TEXT UNIQUE NOT NULL, + title TEXT, + description TEXT, + icon TEXT, + stars INTEGER DEFAULT 0, + + -- Timestamps + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + classified_at TIMESTAMPTZ, + published_at TIMESTAMPTZ, + researched_at TIMESTAMPTZ, + + -- Metadata + reading_time_min INTEGER, + language TEXT, + + -- AI Enrichment + perplexity_research TEXT, + firecrawl_content TEXT, + + -- Insights (different perspectives) + insight_dev TEXT, + insight_founder TEXT, + insight_investor TEXT, + + -- User notes + notes TEXT +); + +-- Index for common queries +CREATE INDEX IF NOT EXISTS idx_bookmarks_classified_at ON bookmarks(classified_at); +CREATE INDEX IF NOT EXISTS idx_bookmarks_published_at ON bookmarks(published_at); +CREATE INDEX IF NOT EXISTS idx_bookmarks_stars ON bookmarks(stars); + +-- Updated at trigger +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER bookmarks_updated_at + BEFORE UPDATE ON bookmarks + FOR EACH ROW + EXECUTE FUNCTION update_updated_at(); +\`\`\` + +### 2. Bookmark Tags Table + +\`\`\`sql +CREATE TABLE IF NOT EXISTS bookmark_tags ( + id SERIAL PRIMARY KEY, + bookmark_id INTEGER REFERENCES bookmarks(id) ON DELETE CASCADE, + tag TEXT NOT NULL, + UNIQUE(bookmark_id, tag) +); + +-- Index for tag lookups +CREATE INDEX IF NOT EXISTS idx_bookmark_tags_tag ON bookmark_tags(tag); +CREATE INDEX IF NOT EXISTS idx_bookmark_tags_bookmark_id ON bookmark_tags(bookmark_id); +\`\`\` + +### 3. Enable Row Level Security (Optional) + +\`\`\`sql +-- Enable RLS +ALTER TABLE bookmarks ENABLE ROW LEVEL SECURITY; +ALTER TABLE bookmark_tags ENABLE ROW LEVEL SECURITY; + +-- Allow public read access (adjust as needed) +CREATE POLICY "Public read access" ON bookmarks + FOR SELECT USING (true); + +CREATE POLICY "Public read access" ON bookmark_tags + FOR SELECT USING (true); +\`\`\` + +## Verification + +After running, verify with: +\`\`\`sql +SELECT table_name FROM information_schema.tables +WHERE table_schema = 'public' +AND table_name IN ('bookmarks', 'bookmark_tags'); +\`\`\` + +## Next Steps + +Once tables are created: +1. Use BOOKMARK_CREATE to add bookmarks +2. Use BOOKMARK_ENRICH_BATCH to research existing bookmarks +3. Use BOOKMARK_IMPORT_CHROME to import from browser`, + }, + }, + ], + }; + }, + }); + +/** + * ENRICH_WORKFLOW - Workflow for bulk enriching bookmarks + */ +export const createEnrichWorkflowPrompt = (_env: Env) => + createPrompt({ + name: "ENRICH_WORKFLOW", + title: "Enrich Bookmarks Workflow", + description: `Workflow for bulk enriching bookmarks with AI research and content scraping.`, + argsSchema: { + batchSize: z + .number() + .optional() + .default(10) + .describe("Number of bookmarks to enrich per batch"), + filterTag: z + .string() + .optional() + .describe("Only enrich bookmarks with this tag"), + }, + execute: ({ args }): GetPromptResult => { + const { batchSize, filterTag } = args; + + return { + description: "Bulk enrich bookmarks with AI", + messages: [ + { + role: "user", + content: { + type: "text", + text: `# Enrich Bookmarks Workflow + +## Goal +Enrich bookmarks that haven't been researched yet with AI-generated insights. + +## Step 1: Find Un-enriched Bookmarks + +Use BOOKMARK_LIST to find bookmarks where: +- researched_at is null +${filterTag ? `- has tag: ${filterTag}` : ""} + +Limit to ${batchSize} bookmarks per batch. + +## Step 2: For Each Bookmark + +### 2a. Scrape Content (if FIRECRAWL available) +\`\`\` +BOOKMARK_SCRAPE(url: bookmark.url) +\`\`\` + +This extracts the main content from the page. + +### 2b. Research with AI (if PERPLEXITY available) +\`\`\` +BOOKMARK_RESEARCH(url: bookmark.url) +\`\`\` + +This generates: +- Summary of what the page is about +- Key insights +- Relevance assessment + +### 2c. Classify and Tag +\`\`\` +BOOKMARK_CLASSIFY(id: bookmark.id) +\`\`\` + +This auto-generates: +- Tags based on content +- Reading time estimate +- Language detection +- Insights from different perspectives (dev, founder, investor) + +## Step 3: Update Bookmark + +Use BOOKMARK_UPDATE to save: +- firecrawl_content +- perplexity_research +- insight_dev, insight_founder, insight_investor +- tags +- classified_at = now() +- researched_at = now() + +## Step 4: Report Progress + +After each batch: +"Enriched X bookmarks. Y remaining without research. + +Would you like to continue with the next batch?" + +## Error Handling + +If a bookmark fails: +- Log the error +- Continue with next bookmark +- Report failures at end`, + }, + }, + ], + }; + }, + }); + +/** + * All prompt factory functions. + */ +export const prompts = [createSetupTablesPrompt, createEnrichWorkflowPrompt]; diff --git a/bookmarks/server/serve.ts b/bookmarks/server/serve.ts new file mode 100644 index 00000000..ea84e9b3 --- /dev/null +++ b/bookmarks/server/serve.ts @@ -0,0 +1,186 @@ +#!/usr/bin/env bun +/** + * Bookmarks MCP - Serve & Link + * + * Starts the bookmarks MCP and exposes it via deco link with a ready-to-add URL. + * + * Usage: + * bun run serve # Start and expose via tunnel + * bun run serve --port 8080 # Custom port + */ + +import { spawn } from "node:child_process"; +import { platform } from "node:os"; +import { resolve } from "node:path"; + +const DEFAULT_PORT = 8011; + +/** + * Parse CLI arguments + */ +function parseArgs(): { port: number } { + const args = process.argv.slice(2); + let port = parseInt(process.env.PORT || String(DEFAULT_PORT), 10); + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === "serve") continue; + + if (arg === "--port" || arg === "-p") { + const p = parseInt(args[++i], 10); + if (!isNaN(p)) port = p; + continue; + } + } + + return { port }; +} + +/** + * Copy text to clipboard + */ +function copyToClipboard(text: string): Promise { + return new Promise((resolvePromise) => { + const os = platform(); + let cmd: string; + let args: string[]; + + if (os === "darwin") { + cmd = "pbcopy"; + args = []; + } else if (os === "win32") { + cmd = "clip"; + args = []; + } else { + cmd = "xclip"; + args = ["-selection", "clipboard"]; + } + + try { + const proc = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] }); + proc.stdin?.write(text); + proc.stdin?.end(); + proc.on("close", (code) => resolvePromise(code === 0)); + proc.on("error", () => resolvePromise(false)); + } catch { + resolvePromise(false); + } + }); +} + +const { port } = parseArgs(); + +let publicUrl = ""; + +/** + * Show the MCP URL banner when we detect the tunnel URL + */ +async function showMcpUrl(tunnelUrl: string) { + if (publicUrl) return; + publicUrl = tunnelUrl; + + const mcpUrl = `${publicUrl}/mcp`; + + const copied = await copyToClipboard(mcpUrl); + + console.log(` + +╔═══════════════════════════════════════════════════════════════════════╗ +║ ✅ Bookmarks MCP Ready! ║ +╠═══════════════════════════════════════════════════════════════════════╣ +║ ║ +║ Add this MCP URL to your Deco Mesh: ║ +║ ║ +║ ${mcpUrl.padEnd(67)}║ +║ ║ +║ ${copied ? "📋 Copied to clipboard!" : "Copy the URL above"} ║ +║ ║ +║ Steps: ║ +║ 1. Open mesh-admin.decocms.com ║ +║ 2. Go to Connections → Add Custom MCP ║ +║ 3. Paste the URL above ║ +║ ║ +╚═══════════════════════════════════════════════════════════════════════╝ +`); +} + +/** + * Check output for tunnel URL + */ +function checkForTunnelUrl(output: string) { + const urlMatch = output.match(/https:\/\/[^\s()"']+\.deco\.(site|host)/); + if (urlMatch) { + const url = urlMatch[0].replace(/[()]/g, ""); + showMcpUrl(url); + } +} + +console.log(` +╔═══════════════════════════════════════════════════════════════════════╗ +║ Bookmarks MCP - Serve & Link ║ +╠═══════════════════════════════════════════════════════════════════════╣ +║ 🔌 Port: ${port.toString().padEnd(61)}║ +║ ║ +║ ⚠️ Note: Only ONE deco link tunnel can run at a time per machine. ║ +║ Stop other 'serve' commands before starting this one. ║ +╚═══════════════════════════════════════════════════════════════════════╝ + +Starting server and tunnel... +`); + +// Get the directory of this script to find main.ts +const scriptDir = import.meta.dirname || resolve(process.cwd(), "server"); +const mainScript = resolve(scriptDir, "main.ts"); + +// Start the MCP server in background +const serverProcess = spawn("bun", ["run", "--hot", mainScript], { + stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, PORT: port.toString() }, +}); + +serverProcess.stdout?.on("data", (data) => { + process.stdout.write(data); +}); + +serverProcess.stderr?.on("data", (data) => { + process.stderr.write(data); +}); + +// Wait for server to start +await new Promise((r) => setTimeout(r, 2000)); + +// Run deco link to get the public URL +const decoLink = spawn("deco", ["link", "-p", port.toString()], { + stdio: ["inherit", "pipe", "pipe"], +}); + +decoLink.stdout?.on("data", (data) => { + const output = data.toString(); + process.stdout.write(output); + checkForTunnelUrl(output); +}); + +decoLink.stderr?.on("data", (data) => { + const output = data.toString(); + process.stderr.write(output); + checkForTunnelUrl(output); +}); + +// Handle exit +process.on("SIGINT", () => { + serverProcess.kill(); + decoLink.kill(); + process.exit(0); +}); + +process.on("SIGTERM", () => { + serverProcess.kill(); + decoLink.kill(); + process.exit(0); +}); + +decoLink.on("close", (code) => { + serverProcess.kill(); + process.exit(code || 0); +}); diff --git a/bookmarks/server/tools/crud.ts b/bookmarks/server/tools/crud.ts new file mode 100644 index 00000000..00740f50 --- /dev/null +++ b/bookmarks/server/tools/crud.ts @@ -0,0 +1,419 @@ +/** + * Bookmark CRUD Tools + * + * Basic create, read, update, delete operations for bookmarks. + * Requires SUPABASE binding. + */ + +import { createTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../types/env.ts"; + +/** + * BOOKMARK_LIST - List bookmarks with optional filters + */ +export const createBookmarkListTool = (env: Env) => + createTool({ + id: "BOOKMARK_LIST", + description: `List bookmarks with optional filters. + +Filters: +- tag: Filter by tag +- hasResearch: Filter by whether bookmark has been researched +- stars: Filter by minimum stars +- limit: Maximum results (default 50)`, + inputSchema: z.object({ + tag: z.string().optional().describe("Filter by tag"), + hasResearch: z.boolean().optional().describe("Filter by research status"), + minStars: z.number().optional().describe("Minimum stars"), + limit: z.number().optional().default(50).describe("Maximum results"), + offset: z + .number() + .optional() + .default(0) + .describe("Offset for pagination"), + }), + handler: async ({ input }) => { + const supabase = env.bindings?.SUPABASE; + + if (!supabase) { + return { + success: false, + error: "SUPABASE binding not configured", + }; + } + + try { + // Build query via Supabase binding + let query = ` + SELECT b.*, + array_agg(t.tag) FILTER (WHERE t.tag IS NOT NULL) as tags + FROM bookmarks b + LEFT JOIN bookmark_tags t ON t.bookmark_id = b.id + WHERE 1=1 + `; + + const params: unknown[] = []; + let paramIndex = 1; + + if (input.hasResearch !== undefined) { + if (input.hasResearch) { + query += ` AND b.researched_at IS NOT NULL`; + } else { + query += ` AND b.researched_at IS NULL`; + } + } + + if (input.minStars !== undefined) { + query += ` AND b.stars >= $${paramIndex++}`; + params.push(input.minStars); + } + + query += ` GROUP BY b.id ORDER BY b.id DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`; + params.push(input.limit, input.offset); + + const result = await supabase.call("execute_sql", { + query, + params, + }); + + // 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!), + ); + } + + return { + success: true, + bookmarks, + count: bookmarks.length, + hasMore: bookmarks.length === input.limit, + }; + } catch (error) { + return { + success: false, + error: `Failed to list bookmarks: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BOOKMARK_GET - Get a single bookmark by URL or ID + */ +export const createBookmarkGetTool = (env: Env) => + createTool({ + id: "BOOKMARK_GET", + description: "Get a single bookmark by URL or ID.", + inputSchema: z.object({ + url: z.string().optional().describe("Bookmark URL"), + id: z.number().optional().describe("Bookmark ID"), + }), + handler: async ({ input }) => { + if (!input.url && !input.id) { + return { success: false, error: "Either url or id is required" }; + } + + const supabase = env.bindings?.SUPABASE; + if (!supabase) { + return { success: false, error: "SUPABASE binding not configured" }; + } + + try { + const query = input.id + ? `SELECT b.*, array_agg(t.tag) FILTER (WHERE t.tag IS NOT NULL) as tags + FROM bookmarks b + LEFT JOIN bookmark_tags t ON t.bookmark_id = b.id + WHERE b.id = $1 + GROUP BY b.id` + : `SELECT b.*, array_agg(t.tag) FILTER (WHERE t.tag IS NOT NULL) as tags + FROM bookmarks b + LEFT JOIN bookmark_tags t ON t.bookmark_id = b.id + WHERE b.url = $1 + GROUP BY b.id`; + + const result = await supabase.call("execute_sql", { + query, + params: [input.id || input.url], + }); + + if (!result.rows || result.rows.length === 0) { + return { success: false, error: "Bookmark not found" }; + } + + return { success: true, bookmark: result.rows[0] }; + } catch (error) { + return { + success: false, + error: `Failed to get bookmark: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BOOKMARK_CREATE - Create a new bookmark + */ +export const createBookmarkCreateTool = (env: Env) => + createTool({ + id: "BOOKMARK_CREATE", + description: "Create a new bookmark.", + inputSchema: z.object({ + url: z.string().describe("Bookmark URL"), + title: z.string().optional().describe("Title"), + description: z.string().optional().describe("Description"), + tags: z.array(z.string()).optional().describe("Tags"), + stars: z.number().optional().default(0).describe("Star rating (0-5)"), + notes: z.string().optional().describe("Personal notes"), + }), + handler: async ({ input }) => { + const supabase = env.bindings?.SUPABASE; + if (!supabase) { + return { success: false, error: "SUPABASE binding not configured" }; + } + + try { + // Insert bookmark + const insertQuery = ` + INSERT INTO bookmarks (url, title, description, stars, notes) + VALUES ($1, $2, $3, $4, $5) + RETURNING id + `; + + const result = await supabase.call("execute_sql", { + query: insertQuery, + params: [ + input.url, + input.title || null, + input.description || null, + input.stars, + input.notes || null, + ], + }); + + const bookmarkId = result.rows[0].id; + + // Insert tags if provided + if (input.tags && input.tags.length > 0) { + const tagValues = input.tags + .map((_, i) => `($1, $${i + 2})`) + .join(", "); + const tagQuery = ` + INSERT INTO bookmark_tags (bookmark_id, tag) + VALUES ${tagValues} + ON CONFLICT (bookmark_id, tag) DO NOTHING + `; + + await supabase.call("execute_sql", { + query: tagQuery, + params: [bookmarkId, ...input.tags], + }); + } + + return { + success: true, + id: bookmarkId, + message: `Bookmark created with ID ${bookmarkId}`, + }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + if (errorMsg.includes("duplicate key") || errorMsg.includes("unique")) { + return { + success: false, + error: "Bookmark with this URL already exists", + }; + } + return { + success: false, + error: `Failed to create bookmark: ${errorMsg}`, + }; + } + }, + }); + +/** + * BOOKMARK_UPDATE - Update an existing bookmark + */ +export const createBookmarkUpdateTool = (env: Env) => + createTool({ + id: "BOOKMARK_UPDATE", + description: + "Update an existing bookmark. Only provided fields are updated.", + inputSchema: z.object({ + id: z.number().describe("Bookmark ID"), + title: z.string().optional(), + description: z.string().optional(), + stars: z.number().optional(), + notes: z.string().optional(), + tags: z.array(z.string()).optional(), + perplexity_research: z.string().optional(), + firecrawl_content: z.string().optional(), + insight_dev: z.string().optional(), + insight_founder: z.string().optional(), + insight_investor: z.string().optional(), + reading_time_min: z.number().optional(), + language: z.string().optional(), + classified_at: z.string().optional().describe("ISO timestamp"), + researched_at: z.string().optional().describe("ISO timestamp"), + }), + handler: async ({ input }) => { + const supabase = env.bindings?.SUPABASE; + if (!supabase) { + return { success: false, error: "SUPABASE binding not configured" }; + } + + try { + const { id, tags, ...fields } = input; + + // Build dynamic update query + const updates: string[] = []; + const params: unknown[] = []; + let paramIndex = 1; + + for (const [key, value] of Object.entries(fields)) { + if (value !== undefined) { + updates.push(`${key} = $${paramIndex++}`); + params.push(value); + } + } + + if (updates.length > 0) { + params.push(id); + const updateQuery = ` + UPDATE bookmarks + SET ${updates.join(", ")} + WHERE id = $${paramIndex} + `; + + await supabase.call("execute_sql", { + query: updateQuery, + params, + }); + } + + // Update tags if provided + if (tags !== undefined) { + // Delete existing tags + await supabase.call("execute_sql", { + query: "DELETE FROM bookmark_tags WHERE bookmark_id = $1", + params: [id], + }); + + // Insert new tags + if (tags.length > 0) { + const tagValues = tags.map((_, i) => `($1, $${i + 2})`).join(", "); + await supabase.call("execute_sql", { + query: `INSERT INTO bookmark_tags (bookmark_id, tag) VALUES ${tagValues}`, + params: [id, ...tags], + }); + } + } + + return { success: true, message: `Bookmark ${id} updated` }; + } catch (error) { + return { + success: false, + error: `Failed to update bookmark: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BOOKMARK_DELETE - Delete a bookmark + */ +export const createBookmarkDeleteTool = (env: Env) => + createTool({ + id: "BOOKMARK_DELETE", + description: "Delete a bookmark by ID.", + inputSchema: z.object({ + id: z.number().describe("Bookmark ID to delete"), + }), + handler: async ({ input }) => { + const supabase = env.bindings?.SUPABASE; + if (!supabase) { + return { success: false, error: "SUPABASE binding not configured" }; + } + + try { + await supabase.call("execute_sql", { + query: "DELETE FROM bookmarks WHERE id = $1", + params: [input.id], + }); + + return { success: true, message: `Bookmark ${input.id} deleted` }; + } catch (error) { + return { + success: false, + error: `Failed to delete bookmark: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BOOKMARK_SEARCH - Full-text search across bookmarks + */ +export const createBookmarkSearchTool = (env: Env) => + createTool({ + id: "BOOKMARK_SEARCH", + description: + "Full-text search across bookmark titles, descriptions, content, and research.", + inputSchema: z.object({ + query: z.string().describe("Search query"), + limit: z.number().optional().default(20).describe("Maximum results"), + }), + handler: async ({ input }) => { + const supabase = env.bindings?.SUPABASE; + if (!supabase) { + return { success: false, error: "SUPABASE binding not configured" }; + } + + try { + const searchTerm = `%${input.query.toLowerCase()}%`; + + const result = await supabase.call("execute_sql", { + query: ` + SELECT b.id, b.url, b.title, b.description, b.stars, b.classified_at, + array_agg(t.tag) FILTER (WHERE t.tag IS NOT NULL) as tags + FROM bookmarks b + LEFT JOIN bookmark_tags t ON t.bookmark_id = b.id + WHERE LOWER(b.title) LIKE $1 + OR LOWER(b.description) LIKE $1 + OR LOWER(b.url) LIKE $1 + OR LOWER(b.perplexity_research) LIKE $1 + OR LOWER(b.firecrawl_content) LIKE $1 + GROUP BY b.id + ORDER BY b.stars DESC, b.id DESC + LIMIT $2 + `, + params: [searchTerm, input.limit], + }); + + return { + success: true, + results: result.rows || [], + count: result.rows?.length || 0, + }; + } catch (error) { + return { + success: false, + error: `Search failed: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * All CRUD tool factories + */ +export const crudTools = [ + createBookmarkListTool, + createBookmarkGetTool, + createBookmarkCreateTool, + createBookmarkUpdateTool, + createBookmarkDeleteTool, + createBookmarkSearchTool, +]; diff --git a/bookmarks/server/tools/enrichment.ts b/bookmarks/server/tools/enrichment.ts new file mode 100644 index 00000000..ccecebba --- /dev/null +++ b/bookmarks/server/tools/enrichment.ts @@ -0,0 +1,400 @@ +/** + * Bookmark Enrichment Tools + * + * AI-powered enrichment using Perplexity and Firecrawl bindings. + */ + +import { createTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../types/env.ts"; + +/** + * BOOKMARK_RESEARCH - Research a bookmark using Perplexity + */ +export const createBookmarkResearchTool = (env: Env) => + createTool({ + id: "BOOKMARK_RESEARCH", + description: `Research a bookmark URL using Perplexity AI. + +Returns a summary of what the page is about, key insights, and relevance assessment. + +Requires PERPLEXITY binding.`, + inputSchema: z.object({ + url: z.string().describe("URL to research"), + context: z + .string() + .optional() + .describe("Additional context for the research"), + }), + handler: async ({ input }) => { + const perplexity = env.bindings?.PERPLEXITY; + + if (!perplexity) { + return { + success: false, + error: + "PERPLEXITY binding not configured. Connect the Perplexity MCP to enable research.", + }; + } + + try { + const prompt = `Research this URL and provide a comprehensive summary: + +URL: ${input.url} +${input.context ? `Context: ${input.context}` : ""} + +Please provide: +1. A 2-3 sentence summary of what this page/resource is about +2. Key insights or takeaways (3-5 bullet points) +3. Who would benefit from this resource +4. Any notable quotes or statistics +5. Related topics or resources`; + + const result = await perplexity.call("PERPLEXITY_SEARCH", { + query: prompt, + }); + + return { + success: true, + research: result.answer || result.text || result, + sources: result.sources || [], + }; + } catch (error) { + return { + success: false, + error: `Research failed: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BOOKMARK_SCRAPE - Scrape bookmark content using Firecrawl + */ +export const createBookmarkScrapeTool = (env: Env) => + createTool({ + id: "BOOKMARK_SCRAPE", + description: `Scrape the content of a bookmark URL using Firecrawl. + +Extracts the main content from the page in markdown format. + +Requires FIRECRAWL binding.`, + inputSchema: z.object({ + url: z.string().describe("URL to scrape"), + }), + handler: async ({ input }) => { + const firecrawl = env.bindings?.FIRECRAWL; + + if (!firecrawl) { + return { + success: false, + error: + "FIRECRAWL binding not configured. Connect the Firecrawl MCP to enable scraping.", + }; + } + + try { + const result = await firecrawl.call("FIRECRAWL_SCRAPE", { + url: input.url, + }); + + return { + success: true, + content: result.markdown || result.content || result, + title: result.title, + metadata: result.metadata, + }; + } catch (error) { + return { + success: false, + error: `Scraping failed: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BOOKMARK_CLASSIFY - Auto-classify a bookmark with tags and insights + */ +export const createBookmarkClassifyTool = (env: Env) => + createTool({ + id: "BOOKMARK_CLASSIFY", + description: `Auto-classify a bookmark with tags and generate insights from multiple perspectives. + +This tool: +1. Analyzes the bookmark content +2. Generates relevant tags +3. Creates insights from developer, founder, and investor perspectives +4. Estimates reading time +5. Detects language + +Requires PERPLEXITY binding for AI analysis.`, + inputSchema: z.object({ + id: z.number().describe("Bookmark ID to classify"), + }), + handler: async ({ input }) => { + const supabase = env.bindings?.SUPABASE; + const perplexity = env.bindings?.PERPLEXITY; + + if (!supabase) { + return { success: false, error: "SUPABASE binding not configured" }; + } + + if (!perplexity) { + return { + success: false, + error: "PERPLEXITY binding not configured for classification", + }; + } + + try { + // Get bookmark + const result = await supabase.call("execute_sql", { + query: + "SELECT url, title, description, firecrawl_content, perplexity_research FROM bookmarks WHERE id = $1", + params: [input.id], + }); + + if (!result.rows || result.rows.length === 0) { + return { success: false, error: "Bookmark not found" }; + } + + const bookmark = result.rows[0]; + const content = + bookmark.firecrawl_content || + bookmark.perplexity_research || + bookmark.description || + bookmark.title; + + if (!content) { + return { + success: false, + error: + "No content available for classification. Run BOOKMARK_SCRAPE or BOOKMARK_RESEARCH first.", + }; + } + + // Classify using Perplexity + const classifyPrompt = `Analyze this content and provide classification: + +URL: ${bookmark.url} +Title: ${bookmark.title || "Unknown"} +Content: ${content.slice(0, 3000)} + +Respond in JSON format: +{ + "tags": ["tag1", "tag2", "tag3"], // 3-7 relevant tags + "language": "en", // ISO language code + "reading_time_min": 5, // estimated reading time + "insight_dev": "Brief insight for developers (1-2 sentences)", + "insight_founder": "Brief insight for founders/entrepreneurs (1-2 sentences)", + "insight_investor": "Brief insight for investors (1-2 sentences)" +}`; + + const classifyResult = await perplexity.call("PERPLEXITY_SEARCH", { + query: classifyPrompt, + }); + + // Parse the response + let classification; + try { + const responseText = + classifyResult.answer || + classifyResult.text || + String(classifyResult); + const jsonMatch = responseText.match(/\{[\s\S]*\}/); + if (jsonMatch) { + classification = JSON.parse(jsonMatch[0]); + } else { + throw new Error("No JSON found in response"); + } + } catch { + return { + success: false, + error: "Failed to parse classification response", + raw: classifyResult, + }; + } + + return { + success: true, + classification, + message: `Bookmark ${input.id} classified with ${classification.tags?.length || 0} tags`, + }; + } catch (error) { + return { + success: false, + error: `Classification failed: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BOOKMARK_ENRICH_BATCH - Batch enrich multiple bookmarks + */ +export const createBookmarkEnrichBatchTool = (env: Env) => + createTool({ + id: "BOOKMARK_ENRICH_BATCH", + description: `Batch enrich bookmarks that haven't been researched yet. + +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.`, + inputSchema: z.object({ + limit: z + .number() + .optional() + .default(5) + .describe("Maximum bookmarks to process"), + skipScrape: z + .boolean() + .optional() + .default(false) + .describe("Skip Firecrawl scraping"), + skipResearch: z + .boolean() + .optional() + .default(false) + .describe("Skip Perplexity research"), + }), + handler: async ({ input }) => { + const supabase = env.bindings?.SUPABASE; + const perplexity = env.bindings?.PERPLEXITY; + const firecrawl = env.bindings?.FIRECRAWL; + + if (!supabase) { + return { success: false, error: "SUPABASE binding not configured" }; + } + + try { + // Get un-enriched bookmarks + const result = await supabase.call("execute_sql", { + query: ` + SELECT id, url, title + FROM bookmarks + WHERE researched_at IS NULL + ORDER BY id + LIMIT $1 + `, + params: [input.limit], + }); + + const bookmarks = result.rows || []; + if (bookmarks.length === 0) { + return { + success: true, + message: "No un-enriched bookmarks found", + processed: 0, + }; + } + + const results: Array<{ + id: number; + url: string; + status: "success" | "partial" | "failed"; + error?: string; + }> = []; + + for (const bookmark of bookmarks) { + try { + let scraped = false; + let researched = false; + + // Scrape content + if (!input.skipScrape && firecrawl) { + try { + const scrapeResult = await firecrawl.call("FIRECRAWL_SCRAPE", { + url: bookmark.url, + }); + if (scrapeResult.markdown || scrapeResult.content) { + await supabase.call("execute_sql", { + query: + "UPDATE bookmarks SET firecrawl_content = $1 WHERE id = $2", + params: [ + scrapeResult.markdown || scrapeResult.content, + bookmark.id, + ], + }); + scraped = true; + } + } catch { + // Continue even if scraping fails + } + } + + // Research with Perplexity + if (!input.skipResearch && perplexity) { + try { + const researchResult = await perplexity.call( + "PERPLEXITY_SEARCH", + { + query: `Summarize this URL in 2-3 sentences: ${bookmark.url}`, + }, + ); + const research = + researchResult.answer || + researchResult.text || + String(researchResult); + await supabase.call("execute_sql", { + query: ` + UPDATE bookmarks + SET perplexity_research = $1, researched_at = NOW() + WHERE id = $2 + `, + params: [research, bookmark.id], + }); + researched = true; + } catch { + // Continue even if research fails + } + } + + results.push({ + id: bookmark.id, + url: bookmark.url, + status: scraped || researched ? "success" : "partial", + }); + } catch (error) { + results.push({ + id: bookmark.id, + url: bookmark.url, + status: "failed", + error: error instanceof Error ? error.message : String(error), + }); + } + } + + const successCount = results.filter( + (r) => r.status === "success", + ).length; + const failedCount = results.filter((r) => r.status === "failed").length; + + return { + success: true, + processed: results.length, + successful: successCount, + failed: failedCount, + results, + }; + } catch (error) { + return { + success: false, + error: `Batch enrichment failed: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * All enrichment tool factories + */ +export const enrichmentTools = [ + createBookmarkResearchTool, + createBookmarkScrapeTool, + createBookmarkClassifyTool, + createBookmarkEnrichBatchTool, +]; diff --git a/bookmarks/server/tools/import.ts b/bookmarks/server/tools/import.ts new file mode 100644 index 00000000..e6b583b2 --- /dev/null +++ b/bookmarks/server/tools/import.ts @@ -0,0 +1,274 @@ +/** + * Bookmark Import Tools + * + * Import bookmarks from browser exports (Chrome, Firefox). + */ + +import { createTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../types/env.ts"; + +/** + * Parse Chrome bookmark export (HTML format) + */ +function parseChromeBookmarks(html: string): Array<{ + url: string; + title: string; + addedAt?: Date; +}> { + const bookmarks: Array<{ url: string; title: string; addedAt?: Date }> = []; + + // Match title + const regex = + /]*(?:ADD_DATE="(\d+)")?[^>]*>([^<]*)<\/A>/gi; + let match; + + while ((match = regex.exec(html)) !== null) { + const url = match[1]; + const addDate = match[2]; + const title = match[3].trim(); + + // Skip javascript: and chrome: URLs + if (url.startsWith("javascript:") || url.startsWith("chrome:")) { + continue; + } + + bookmarks.push({ + url, + title: title || url, + addedAt: addDate ? new Date(parseInt(addDate, 10) * 1000) : undefined, + }); + } + + return bookmarks; +} + +/** + * Parse Firefox bookmark export (JSON format from about:support or HTML) + */ +function parseFirefoxBookmarks( + content: string, +): Array<{ url: string; title: string; addedAt?: Date }> { + const bookmarks: Array<{ url: string; title: string; addedAt?: Date }> = []; + + // Try JSON format first (from about:support) + try { + const data = JSON.parse(content); + + function extractFromJson(node: { + uri?: string; + title?: string; + dateAdded?: number; + children?: unknown[]; + }) { + if (node.uri && !node.uri.startsWith("place:")) { + bookmarks.push({ + url: node.uri, + title: node.title || node.uri, + addedAt: node.dateAdded ? new Date(node.dateAdded / 1000) : undefined, + }); + } + if (node.children && Array.isArray(node.children)) { + for (const child of node.children) { + extractFromJson(child as typeof node); + } + } + } + + extractFromJson(data); + return bookmarks; + } catch { + // Not JSON, try HTML format (same as Chrome) + return parseChromeBookmarks(content); + } +} + +/** + * BOOKMARK_IMPORT_CHROME - Import bookmarks from Chrome HTML export + */ +export const createBookmarkImportChromeTool = (env: Env) => + createTool({ + id: "BOOKMARK_IMPORT_CHROME", + description: `Import bookmarks from a Chrome HTML export file. + +To export from Chrome: +1. Open chrome://bookmarks +2. Click the three dots menu +3. Select "Export bookmarks" + +Provide the HTML content from the exported file.`, + inputSchema: z.object({ + html: z.string().describe("HTML content from Chrome bookmark export"), + skipDuplicates: z + .boolean() + .optional() + .default(true) + .describe("Skip bookmarks that already exist"), + }), + handler: async ({ input }) => { + const supabase = env.bindings?.SUPABASE; + if (!supabase) { + return { success: false, error: "SUPABASE binding not configured" }; + } + + try { + const bookmarks = parseChromeBookmarks(input.html); + + if (bookmarks.length === 0) { + return { + success: false, + error: "No bookmarks found in the HTML content", + }; + } + + let imported = 0; + let skipped = 0; + const errors: string[] = []; + + for (const bookmark of bookmarks) { + try { + if (input.skipDuplicates) { + // Check if exists + const existing = await supabase.call("execute_sql", { + query: "SELECT id FROM bookmarks WHERE url = $1", + params: [bookmark.url], + }); + if (existing.rows && existing.rows.length > 0) { + skipped++; + continue; + } + } + + await supabase.call("execute_sql", { + query: ` + INSERT INTO bookmarks (url, title, created_at) + VALUES ($1, $2, $3) + ON CONFLICT (url) DO NOTHING + `, + params: [ + bookmark.url, + bookmark.title, + bookmark.addedAt?.toISOString() || new Date().toISOString(), + ], + }); + imported++; + } catch (error) { + errors.push( + `${bookmark.url}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + return { + success: true, + total: bookmarks.length, + imported, + skipped, + errors: errors.length > 0 ? errors.slice(0, 10) : undefined, + }; + } catch (error) { + return { + success: false, + error: `Import failed: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BOOKMARK_IMPORT_FIREFOX - Import bookmarks from Firefox export + */ +export const createBookmarkImportFirefoxTool = (env: Env) => + createTool({ + id: "BOOKMARK_IMPORT_FIREFOX", + description: `Import bookmarks from a Firefox export. + +Supports both: +- HTML export (from Library > Import and Backup > Export Bookmarks to HTML) +- JSON export (from about:support) + +Provide the file content.`, + inputSchema: z.object({ + content: z.string().describe("Content from Firefox bookmark export"), + skipDuplicates: z + .boolean() + .optional() + .default(true) + .describe("Skip bookmarks that already exist"), + }), + handler: async ({ input }) => { + const supabase = env.bindings?.SUPABASE; + if (!supabase) { + return { success: false, error: "SUPABASE binding not configured" }; + } + + try { + const bookmarks = parseFirefoxBookmarks(input.content); + + if (bookmarks.length === 0) { + return { + success: false, + error: "No bookmarks found in the content", + }; + } + + let imported = 0; + let skipped = 0; + const errors: string[] = []; + + for (const bookmark of bookmarks) { + try { + if (input.skipDuplicates) { + const existing = await supabase.call("execute_sql", { + query: "SELECT id FROM bookmarks WHERE url = $1", + params: [bookmark.url], + }); + if (existing.rows && existing.rows.length > 0) { + skipped++; + continue; + } + } + + await supabase.call("execute_sql", { + query: ` + INSERT INTO bookmarks (url, title, created_at) + VALUES ($1, $2, $3) + ON CONFLICT (url) DO NOTHING + `, + params: [ + bookmark.url, + bookmark.title, + bookmark.addedAt?.toISOString() || new Date().toISOString(), + ], + }); + imported++; + } catch (error) { + errors.push( + `${bookmark.url}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + return { + success: true, + total: bookmarks.length, + imported, + skipped, + errors: errors.length > 0 ? errors.slice(0, 10) : undefined, + }; + } catch (error) { + return { + success: false, + error: `Import failed: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * All import tool factories + */ +export const importTools = [ + createBookmarkImportChromeTool, + createBookmarkImportFirefoxTool, +]; diff --git a/bookmarks/server/tools/index.ts b/bookmarks/server/tools/index.ts new file mode 100644 index 00000000..a931602d --- /dev/null +++ b/bookmarks/server/tools/index.ts @@ -0,0 +1,22 @@ +/** + * Bookmarks MCP Tools + * + * Aggregates all bookmark tools: + * - CRUD: List, Get, Create, Update, Delete, Search + * - Enrichment: Research, Scrape, Classify, EnrichBatch + * - Import: ImportChrome, ImportFirefox + */ + +import { crudTools } from "./crud.ts"; +import { enrichmentTools } from "./enrichment.ts"; +import { importTools } from "./import.ts"; + +/** + * All tool factory functions. + */ +export const tools = [...crudTools, ...enrichmentTools, ...importTools]; + +// Re-export individual modules +export { crudTools } from "./crud.ts"; +export { enrichmentTools } from "./enrichment.ts"; +export { importTools } from "./import.ts"; diff --git a/bookmarks/server/types/env.ts b/bookmarks/server/types/env.ts new file mode 100644 index 00000000..67bfa6a3 --- /dev/null +++ b/bookmarks/server/types/env.ts @@ -0,0 +1,23 @@ +/** + * Environment Type Definitions for Bookmarks MCP + */ +import { + BindingOf, + type DefaultEnv, + type BindingRegistry, +} from "@decocms/runtime"; +import { z } from "zod"; + +export const StateSchema = z.object({ + SUPABASE: BindingOf("@supabase/supabase") + .optional() + .describe("Supabase binding for bookmark storage"), + PERPLEXITY: BindingOf("@deco/perplexity") + .optional() + .describe("Perplexity binding for AI research"), + FIRECRAWL: BindingOf("@deco/firecrawl") + .optional() + .describe("Firecrawl binding for web scraping"), +}); + +export type Env = DefaultEnv; diff --git a/bookmarks/tsconfig.json b/bookmarks/tsconfig.json new file mode 100644 index 00000000..5f2d051f --- /dev/null +++ b/bookmarks/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "verbatimModuleSyntax": false, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "allowJs": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path Aliases */ + "baseUrl": ".", + "paths": { + "server/*": ["./server/*"] + } + }, + "include": [ + "server" + ] +} diff --git a/brand/README.md b/brand/README.md new file mode 100644 index 00000000..fb6fbcbf --- /dev/null +++ b/brand/README.md @@ -0,0 +1,221 @@ +# Brand MCP + +AI-powered brand research and design system generator. Automatically discover brand identity from websites and generate complete design systems. + +## Features + +- **Website Scraping** - Extract colors, fonts, logos directly from websites using Firecrawl +- **AI Research** - Deep brand research using Perplexity AI +- **Design System Generation** - CSS variables, JSX components, markdown style guides +- **MCP Apps UI** - Interactive brand previews in Mesh admin +- **One-Step Creation** - Full workflow from URL to complete design system + +## Quick Start + +```bash +# Start the server +bun run dev + +# The MCP will be available at: +# http://localhost:8001/mcp +``` + +## Required Bindings + +Configure at least one binding for brand research: + +### Firecrawl (`@deco/firecrawl`) +- Extract colors from website CSS +- Identify typography and fonts +- Find logo images in page source +- Capture visual style and aesthetics +- Take screenshots + +### Perplexity (`@deco/perplexity`) +- Research brand history and background +- Find brand guidelines and press kits +- Discover logo URLs and assets +- Analyze brand voice and personality +- Find color palettes from various sources + +## Tools + +### Research Tools + +| Tool | Description | +|------|-------------| +| `BRAND_SCRAPE` | Scrape a website to extract brand identity using Firecrawl | +| `BRAND_RESEARCH` | Deep research on a brand using Perplexity AI | +| `BRAND_DISCOVER` | Combined scraping + research for complete identity | +| `BRAND_STATUS` | Check available research capabilities | + +### Generator Tools + +| Tool | Description | +|------|-------------| +| `BRAND_GENERATE` | Generate design system from brand identity | +| `BRAND_CREATE` | Full workflow: discover + generate in one step | + +## MCP Apps (UI Resources) + +| Resource URI | Description | +|--------------|-------------| +| `ui://brand-preview` | Interactive brand identity preview | +| `ui://brand-list` | Grid view of all created brands | + +## Workflow + +### Quick: One-Step Brand Creation + +``` +BRAND_CREATE(brandName: "Acme Corp", websiteUrl: "https://acme.com") +``` + +Returns: +- Complete brand identity object +- CSS variables file +- JSX design system +- Markdown style guide + +### Detailed: Step-by-Step + +1. **Check Status** + ``` + BRAND_STATUS() + ``` + Verify which bindings are available. + +2. **Discover Brand** + ``` + BRAND_DISCOVER(brandName: "Acme", websiteUrl: "https://acme.com") + ``` + Combines scraping and research for complete identity. + +3. **Generate Design System** + ``` + BRAND_GENERATE(identity: {...}, outputFormat: "all") + ``` + Creates CSS, JSX, and style guide. + +## Output Formats + +### CSS Variables + +```css +:root { + --brand-primary: #8B5CF6; + --brand-primary-light: #A78BFA; + --brand-secondary: #10B981; + --bg-dark: #1a1a1a; + --font-heading: 'Inter', system-ui, sans-serif; + /* ... */ +} +``` + +### JSX Design System + +```jsx +// Brand configuration +const BRAND = { + name: "Acme Corp", + colors: { primary: "#8B5CF6", ... }, + logos: { primary: "https://...", ... }, +}; + +// Components +function BrandLogo({ variant, height }) { ... } +function Heading({ level, children }) { ... } +function Button({ variant, children }) { ... } +function Card({ children }) { ... } +``` + +### Markdown Style Guide + +Complete documentation including: +- Color palette with hex codes +- Typography specifications +- Logo usage guidelines +- Visual style rules +- Brand voice description + +## Brand Identity Schema + +```typescript +interface BrandIdentity { + name: string; + tagline?: string; + description?: string; + industry?: string; + + colors: { + primary: string; // Main brand color + secondary?: string; // Supporting color + accent?: string; // Highlight color + background?: string; // Background color + text?: string; // Text color + palette?: string[]; // Full palette + }; + + logos?: { + primary?: string; // Main logo URL + light?: string; // For dark backgrounds + dark?: string; // For light backgrounds + icon?: string; // Square icon + }; + + typography?: { + headingFont?: string; + bodyFont?: string; + monoFont?: string; + }; + + style?: { + aesthetic?: string; // e.g., "modern", "minimal" + mood?: string; // e.g., "professional", "playful" + keywords?: string[]; + }; + + voice?: { + tone?: string; + personality?: string[]; + values?: string[]; + }; + + confidence: "high" | "medium" | "low"; + sources: string[]; +} +``` + +## Integration with Slides MCP + +The Brand MCP is designed to work seamlessly with the Slides MCP: + +1. Create a brand with `BRAND_CREATE` +2. Use the generated identity with Slides' `DECK_INIT` +3. The design system JSX can be used directly + +## Development + +```bash +# Install dependencies +bun install + +# Run development server +bun run dev + +# Type check +bun run check + +# Build for production +bun run build +``` + +## Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `PORT` | `8001` | Server port | + +## License + +MIT diff --git a/brand/package.json b/brand/package.json new file mode 100644 index 00000000..732b37b0 --- /dev/null +++ b/brand/package.json @@ -0,0 +1,29 @@ +{ + "name": "@decocms/brand", + "version": "1.0.0", + "description": "AI-powered brand research and design system generator", + "private": true, + "type": "module", + "scripts": { + "dev": "bun run --hot server/main.ts", + "build:server": "NODE_ENV=production bun build server/main.ts --target=bun --outfile=dist/server/main.js", + "build": "bun run build:server", + "check": "tsc --noEmit" + }, + "exports": { + "./tools": "./server/tools/index.ts" + }, + "dependencies": { + "@decocms/bindings": "^1.0.9", + "@decocms/runtime": "1.2.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "@decocms/mcps-shared": "workspace:*", + "@modelcontextprotocol/sdk": "1.25.1", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/brand/server/main.ts b/brand/server/main.ts new file mode 100644 index 00000000..a88ed4a2 --- /dev/null +++ b/brand/server/main.ts @@ -0,0 +1,121 @@ +/** + * Brand MCP - AI-Powered Brand Research & Design System Generator + * + * A complete toolset for discovering brand identity and generating design systems. + * + * ## Features + * + * - **Brand Scraping** - Extract colors, fonts, logos from websites using Firecrawl + * - **Brand Research** - Deep research using Perplexity AI + * - **Design System Generation** - CSS variables, JSX components, style guides + * - **MCP Apps UI** - Interactive brand previews + * - **Persistent Storage** - Projects stored to filesystem (official MCP filesystem compatible) + * + * ## Optional Bindings + * + * Configure for full functionality: + * - **FIRECRAWL** - For website scraping and brand extraction (firecrawl-mcp) + * - **PERPLEXITY** - For AI-powered brand research (@perplexity-ai/mcp-server) + * - **FILESYSTEM** - For persistent storage (works with @modelcontextprotocol/server-filesystem or @decocms/mcp-local-fs) + */ +import { withRuntime } from "@decocms/runtime"; +import { tools } from "./tools/index.ts"; +import { resources } from "./resources/index.ts"; +import { StateSchema, type Env, type Registry } from "./types/env.ts"; + +export { StateSchema }; + +const PORT = process.env.PORT || 8003; + +console.log("[brand-mcp] Starting server..."); +console.log("[brand-mcp] Port:", PORT); +console.log("[brand-mcp] Tools count:", tools.length); +console.log("[brand-mcp] Resources count:", resources.length); + +const runtime = withRuntime({ + configuration: { + scopes: ["PERPLEXITY::*", "FIRECRAWL::*", "FILESYSTEM::*"], + state: StateSchema, + }, + tools, + prompts: [], + resources, +}); + +console.log("[brand-mcp] Runtime initialized"); + +/** + * Fetch handler with logging + */ +const fetchWithLogging = async (req: Request): Promise => { + const url = new URL(req.url); + const startTime = Date.now(); + + // Log incoming request + if (req.method === "POST" && url.pathname === "/mcp") { + try { + const body = await req.clone().json(); + const method = body?.method || "unknown"; + const toolName = body?.params?.name; + + if (method === "tools/call" && toolName) { + console.log(`[brand-mcp] 🔧 Tool call: ${toolName}`); + } else if (method !== "unknown") { + console.log(`[brand-mcp] 📨 Request: ${method}`); + } + } catch { + // Ignore JSON parse errors + } + } + + // Call the runtime + const response = await runtime.fetch(req); + + // Log response time for tool calls + const duration = Date.now() - startTime; + if (duration > 100) { + console.log(`[brand-mcp] ⏱️ Response in ${duration}ms`); + } + + return response; +}; + +// Start the server +Bun.serve({ + port: PORT, + hostname: "0.0.0.0", + idleTimeout: 0, // Required for SSE + fetch: fetchWithLogging, + development: process.env.NODE_ENV !== "production", +}); + +console.log(""); +console.log("🎨 Brand MCP running at: http://localhost:" + PORT + "/mcp"); +console.log(""); +console.log("[brand-mcp] Available tools:"); +console.log(" - BRAND_SCRAPE - Extract brand identity from websites"); +console.log(" - BRAND_RESEARCH - Deep research using Perplexity AI"); +console.log(" - BRAND_DISCOVER - Combined scraping + research"); +console.log(" - BRAND_STATUS - Check available capabilities"); +console.log(" - BRAND_GENERATE - Generate design system from identity"); +console.log(" - BRAND_CREATE - Full workflow: discover + generate"); +console.log(""); +console.log("[brand-mcp] MCP Apps (UI Resources):"); +console.log(" - ui://brand-preview - Interactive brand preview"); +console.log(" - ui://brand-list - Grid view of brands"); +console.log(""); +console.log("[brand-mcp] Optional bindings: PERPLEXITY, FIRECRAWL, FILESYSTEM"); + +// Copy URL to clipboard on macOS +if (process.platform === "darwin") { + try { + const proc = Bun.spawn(["pbcopy"], { + stdin: "pipe", + }); + proc.stdin.write(`http://localhost:${PORT}/mcp`); + proc.stdin.end(); + console.log("[brand-mcp] 📋 MCP URL copied to clipboard!"); + } catch { + // Ignore clipboard errors + } +} diff --git a/brand/server/resources/index.ts b/brand/server/resources/index.ts new file mode 100644 index 00000000..5a3abfaf --- /dev/null +++ b/brand/server/resources/index.ts @@ -0,0 +1,572 @@ +/** + * MCP Apps Resources for Brand MCP + * + * Interactive UI resources for displaying brand identities and design systems. + */ +import { createPublicResource } from "@decocms/runtime"; +import type { Env } from "../types/env.ts"; + +/** + * Brand Preview App - Shows brand identity with colors, logos, typography + */ +const BRAND_PREVIEW_HTML = ` + + + + + Brand Preview + + + +
+
+

Brand Preview

+

Waiting for brand data...

+
+
+ + + +`; + +/** + * Brand List App - Shows all created brands + */ +const BRAND_LIST_HTML = ` + + + + + Brand List + + + +
+

Brands

+
+
+

No brands yet. Use BRAND_CREATE to create one.

+
+
+
+ + + +`; + +export const createBrandPreviewResource = (_env: Env) => + createPublicResource({ + uri: "ui://brand-preview", + name: "Brand Preview", + description: "Interactive preview of brand identity and design system", + mimeType: "text/html;profile=mcp-app", + read: () => ({ + uri: "ui://brand-preview", + mimeType: "text/html;profile=mcp-app", + text: BRAND_PREVIEW_HTML, + }), + }); + +export const createBrandListResource = (_env: Env) => + createPublicResource({ + uri: "ui://brand-list", + name: "Brand List", + description: "View all created brands", + mimeType: "text/html;profile=mcp-app", + read: () => ({ + uri: "ui://brand-list", + mimeType: "text/html;profile=mcp-app", + text: BRAND_LIST_HTML, + }), + }); + +export const resources = [createBrandPreviewResource, createBrandListResource]; diff --git a/brand/server/tools/generator-utils.ts b/brand/server/tools/generator-utils.ts new file mode 100644 index 00000000..3f71e34a --- /dev/null +++ b/brand/server/tools/generator-utils.ts @@ -0,0 +1,266 @@ +/** + * Design System Generator Utilities + * + * Pure functions for generating CSS, JSX, and style guides from brand identity. + */ +import type { BrandIdentity } from "./research.ts"; + +// Helper functions +export function lightenColor(hex: string, percent: number): string { + const num = parseInt(hex.replace("#", ""), 16); + const amt = Math.round(2.55 * percent); + const R = Math.min(255, (num >> 16) + amt); + const G = Math.min(255, ((num >> 8) & 0x00ff) + amt); + const B = Math.min(255, (num & 0x0000ff) + amt); + return `#${((1 << 24) + (R << 16) + (G << 8) + B).toString(16).slice(1)}`; +} + +export function darkenColor(hex: string, percent: number): string { + const num = parseInt(hex.replace("#", ""), 16); + const amt = Math.round(2.55 * percent); + const R = Math.max(0, (num >> 16) - amt); + const G = Math.max(0, ((num >> 8) & 0x00ff) - amt); + const B = Math.max(0, (num & 0x0000ff) - amt); + return `#${((1 << 24) + (R << 16) + (G << 8) + B).toString(16).slice(1)}`; +} + +/** + * Generate CSS variables from brand identity + */ +export function generateCSSVariables(identity: BrandIdentity): string { + const colors = identity.colors || { primary: "#8B5CF6" }; + const typography = identity.typography || {}; + + return `/** + * ${identity.name} Design System + * Generated by Brand MCP + */ + +:root { + /* Primary Colors */ + --brand-primary: ${colors.primary}; + --brand-primary-light: ${lightenColor(colors.primary, 20)}; + --brand-primary-dark: ${darkenColor(colors.primary, 20)}; + + /* Secondary Colors */ + --brand-secondary: ${colors.secondary || colors.primary}; + --brand-accent: ${colors.accent || colors.primary}; + + /* Background Colors */ + --bg-dark: ${colors.background || "#1a1a1a"}; + --bg-light: #FFFFFF; + --bg-gray: #F5F5F5; + + /* Text Colors */ + --text-primary: ${colors.text || "#1A1A1A"}; + --text-secondary: #6B7280; + --text-light: #FFFFFF; + --text-muted: #9CA3AF; + + /* Typography */ + --font-heading: ${typography.headingFont || "'Inter', system-ui, sans-serif"}; + --font-body: ${typography.bodyFont || "'Inter', system-ui, sans-serif"}; + --font-mono: ${typography.monoFont || "'JetBrains Mono', monospace"}; + + /* Spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-2xl: 3rem; + + /* Border Radius */ + --radius-sm: 0.25rem; + --radius-md: 0.5rem; + --radius-lg: 1rem; + --radius-full: 9999px; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); +} + +/* Dark Mode */ +[data-theme="dark"] { + --bg-dark: #0f0f0f; + --bg-light: #1a1a1a; + --bg-gray: #2a2a2a; + --text-primary: #FFFFFF; + --text-secondary: #9CA3AF; +} +`; +} + +/** + * Generate JSX design system components + */ +export function generateDesignSystemJSX(identity: BrandIdentity): string { + const colors = identity.colors || { primary: "#8B5CF6" }; + const logos = identity.logos || {}; + const typography = identity.typography || {}; + + return `/** + * ${identity.name} Design System + * + * Auto-generated design system with brand components. + */ + +const BRAND = { + name: "${identity.name}", + tagline: "${identity.tagline || ""}", + + colors: { + primary: "${colors.primary}", + primaryLight: "${lightenColor(colors.primary, 20)}", + primaryDark: "${darkenColor(colors.primary, 20)}", + secondary: "${colors.secondary || colors.primary}", + accent: "${colors.accent || colors.primary}", + background: "${colors.background || "#1a1a1a"}", + text: "${colors.text || "#1A1A1A"}", + textLight: "#FFFFFF", + textMuted: "#6B7280", + }, + + logos: { + primary: ${logos.primary ? `"${logos.primary}"` : "null"}, + light: ${logos.light ? `"${logos.light}"` : "null"}, + dark: ${logos.dark ? `"${logos.dark}"` : "null"}, + icon: ${logos.icon ? `"${logos.icon}"` : "null"}, + }, + + typography: { + heading: "${typography.headingFont || "Inter, system-ui, sans-serif"}", + body: "${typography.bodyFont || "Inter, system-ui, sans-serif"}", + mono: "${typography.monoFont || "JetBrains Mono, monospace"}", + }, + + hasImageLogo: ${Boolean(logos.primary)}, +}; + +function BrandLogo({ variant = "primary", height = 40 }) { + const logoUrl = variant === "light" ? BRAND.logos.light : + variant === "dark" ? BRAND.logos.dark : + BRAND.logos.primary; + + if (logoUrl) { + return {BRAND.name}; + } + + return ( + + {BRAND.name} + + ); +} + +function Heading({ level = 1, children, color = "primary" }) { + const Tag = \`h\${level}\`; + const sizes = { 1: "3rem", 2: "2.25rem", 3: "1.875rem", 4: "1.5rem" }; + const colors = { primary: BRAND.colors.text, brand: BRAND.colors.primary, light: "#FFFFFF" }; + + return {children}; +} + +function Button({ variant = "primary", children }) { + const styles = { + primary: { backgroundColor: BRAND.colors.primary, color: "#FFFFFF" }, + secondary: { backgroundColor: "transparent", color: BRAND.colors.primary, border: \`2px solid \${BRAND.colors.primary}\` }, + }; + + return ; +} + +export { BRAND, BrandLogo, Heading, Button }; +`; +} + +/** + * Generate style guide markdown + */ +export function generateStyleGuide(identity: BrandIdentity): string { + const colors = identity.colors || { primary: "#8B5CF6" }; + const typography = identity.typography || {}; + const voice = identity.voice || {}; + + return `# ${identity.name} Brand Style Guide + +${identity.description ? `> ${identity.description}` : ""} +${identity.tagline ? `**Tagline:** "${identity.tagline}"` : ""} + +--- + +## Color Palette + +### Primary Colors + +| Color | Hex | Usage | +|-------|-----|-------| +| Primary | \`${colors.primary}\` | Main brand color, CTAs, links | +| Primary Light | \`${lightenColor(colors.primary, 20)}\` | Hover states, backgrounds | +| Primary Dark | \`${darkenColor(colors.primary, 20)}\` | Active states, emphasis | + +${ + colors.secondary + ? `### Secondary Colors + +| Color | Hex | Usage | +|-------|-----|-------| +| Secondary | \`${colors.secondary}\` | Supporting elements | +${colors.accent ? `| Accent | \`${colors.accent}\` | Highlights, notifications |` : ""}` + : "" +} + +--- + +## Typography + +### Font Families + +- **Headings:** ${typography.headingFont || "Inter, system-ui, sans-serif"} +- **Body:** ${typography.bodyFont || "Inter, system-ui, sans-serif"} +${typography.monoFont ? `- **Monospace:** ${typography.monoFont}` : ""} + +### Type Scale + +| Element | Size | Weight | +|---------|------|--------| +| H1 | 3rem (48px) | Bold (700) | +| H2 | 2.25rem (36px) | Bold (700) | +| H3 | 1.875rem (30px) | Semibold (600) | +| Body | 1rem (16px) | Regular (400) | + +--- + +## Logo Usage + +${ + identity.logos?.primary + ? `### Primary Logo + +![${identity.name} Logo](${identity.logos.primary}) + +- Use on light backgrounds +- Maintain clear space equal to the height of the logo` + : `### Logo + +*Logo URL not available. Please provide logo assets.*` +} + +--- + +${ + voice.tone || voice.personality?.length + ? `## Brand Voice + +${voice.tone ? `**Tone:** ${voice.tone}` : ""} +${voice.personality?.length ? `**Personality:** ${voice.personality.join(", ")}` : ""} + +---` + : "" +} + +*Generated by Brand MCP* +`; +} diff --git a/brand/server/tools/generator.ts b/brand/server/tools/generator.ts new file mode 100644 index 00000000..ecf8b22f --- /dev/null +++ b/brand/server/tools/generator.ts @@ -0,0 +1,816 @@ +/** + * Design System Generator Tools + * + * Generates design system JSX and style guides from brand identity. + */ +import { createTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../types/env.ts"; +import { type BrandIdentity, BrandIdentitySchema } from "./research.ts"; + +/** + * Generate CSS variables from brand colors + */ +function generateCSSVariables(identity: BrandIdentity): string { + const colors = identity.colors || { primary: "#8B5CF6" }; + const typography = identity.typography || {}; + + return `/** + * ${identity.name} Design System + * Generated by Brand MCP + */ + +:root { + /* Primary Colors */ + --brand-primary: ${colors.primary}; + --brand-primary-light: ${lightenColor(colors.primary, 20)}; + --brand-primary-dark: ${darkenColor(colors.primary, 20)}; + + /* Secondary Colors */ + --brand-secondary: ${colors.secondary || colors.primary}; + --brand-accent: ${colors.accent || colors.primary}; + + /* Background Colors */ + --bg-dark: ${colors.background || "#1a1a1a"}; + --bg-light: #FFFFFF; + --bg-gray: #F5F5F5; + + /* Text Colors */ + --text-primary: ${colors.text || "#1A1A1A"}; + --text-secondary: #6B7280; + --text-light: #FFFFFF; + --text-muted: #9CA3AF; + + /* Typography */ + --font-heading: ${typography.headingFont || "'Inter', system-ui, sans-serif"}; + --font-body: ${typography.bodyFont || "'Inter', system-ui, sans-serif"}; + --font-mono: ${typography.monoFont || "'JetBrains Mono', monospace"}; + + /* Spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-2xl: 3rem; + + /* Border Radius */ + --radius-sm: 0.25rem; + --radius-md: 0.5rem; + --radius-lg: 1rem; + --radius-full: 9999px; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); +} + +/* Dark Mode */ +[data-theme="dark"] { + --bg-dark: #0f0f0f; + --bg-light: #1a1a1a; + --bg-gray: #2a2a2a; + --text-primary: #FFFFFF; + --text-secondary: #9CA3AF; +} +`; +} + +/** + * Generate JSX design system components + */ +function generateDesignSystemJSX(identity: BrandIdentity): string { + const colors = identity.colors || { primary: "#8B5CF6" }; + const logos = identity.logos || {}; + const typography = identity.typography || {}; + + return `/** + * ${identity.name} Design System + * + * Auto-generated design system with brand components. + * Use with Babel Standalone for browser transpilation. + */ + +// Brand configuration +const BRAND = { + name: "${identity.name}", + tagline: "${identity.tagline || ""}", + + colors: { + primary: "${colors.primary}", + primaryLight: "${lightenColor(colors.primary, 20)}", + primaryDark: "${darkenColor(colors.primary, 20)}", + secondary: "${colors.secondary || colors.primary}", + accent: "${colors.accent || colors.primary}", + background: "${colors.background || "#1a1a1a"}", + text: "${colors.text || "#1A1A1A"}", + textLight: "#FFFFFF", + textMuted: "#6B7280", + }, + + logos: { + primary: ${logos.primary ? `"${logos.primary}"` : "null"}, + light: ${logos.light ? `"${logos.light}"` : "null"}, + dark: ${logos.dark ? `"${logos.dark}"` : "null"}, + icon: ${logos.icon ? `"${logos.icon}"` : "null"}, + }, + + typography: { + heading: "${typography.headingFont || "Inter, system-ui, sans-serif"}", + body: "${typography.bodyFont || "Inter, system-ui, sans-serif"}", + mono: "${typography.monoFont || "JetBrains Mono, monospace"}", + }, + + hasImageLogo: ${Boolean(logos.primary)}, +}; + +/** + * Brand Logo Component + * Renders image logo if available, falls back to text + */ +function BrandLogo({ variant = "primary", className = "", height = 40 }) { + const logoUrl = variant === "light" ? BRAND.logos.light : + variant === "dark" ? BRAND.logos.dark : + BRAND.logos.primary; + + if (logoUrl) { + return ( + {BRAND.name} + ); + } + + // Text fallback + return ( + + {BRAND.name} + + ); +} + +/** + * Brand Icon Component + */ +function BrandIcon({ size = 32, className = "" }) { + if (BRAND.logos.icon) { + return ( + {BRAND.name} + ); + } + + // Fallback to first letter + return ( +
+ {BRAND.name[0]} +
+ ); +} + +/** + * Heading Component + */ +function Heading({ level = 1, children, className = "", color = "primary" }) { + const Tag = \`h\${level}\`; + const sizes = { + 1: "3rem", + 2: "2.25rem", + 3: "1.875rem", + 4: "1.5rem", + 5: "1.25rem", + 6: "1rem", + }; + + const colors = { + primary: BRAND.colors.text, + brand: BRAND.colors.primary, + light: BRAND.colors.textLight, + muted: BRAND.colors.textMuted, + }; + + return ( + + {children} + + ); +} + +/** + * Text Component + */ +function Text({ size = "base", children, className = "", color = "primary", weight = "normal" }) { + const sizes = { + xs: "0.75rem", + sm: "0.875rem", + base: "1rem", + lg: "1.125rem", + xl: "1.25rem", + }; + + const colors = { + primary: BRAND.colors.text, + secondary: BRAND.colors.textMuted, + brand: BRAND.colors.primary, + light: BRAND.colors.textLight, + }; + + return ( +

+ {children} +

+ ); +} + +/** + * Button Component + */ +function Button({ variant = "primary", size = "md", children, className = "", ...props }) { + const variants = { + primary: { + backgroundColor: BRAND.colors.primary, + color: BRAND.colors.textLight, + border: "none", + }, + secondary: { + backgroundColor: "transparent", + color: BRAND.colors.primary, + border: \`2px solid \${BRAND.colors.primary}\`, + }, + ghost: { + backgroundColor: "transparent", + color: BRAND.colors.text, + border: "none", + }, + }; + + const sizes = { + sm: { padding: "0.5rem 1rem", fontSize: "0.875rem" }, + md: { padding: "0.75rem 1.5rem", fontSize: "1rem" }, + lg: { padding: "1rem 2rem", fontSize: "1.125rem" }, + }; + + return ( + + ); +} + +/** + * Card Component + */ +function Card({ children, className = "", variant = "default" }) { + const variants = { + default: { + backgroundColor: "#FFFFFF", + border: "1px solid #E5E7EB", + }, + elevated: { + backgroundColor: "#FFFFFF", + boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)", + }, + brand: { + backgroundColor: BRAND.colors.primary, + color: BRAND.colors.textLight, + }, + }; + + return ( +
+ {children} +
+ ); +} + +/** + * Color Swatch Component + */ +function ColorSwatch({ color, name, className = "" }) { + return ( +
+
+ {name} + {color} +
+ ); +} + +// Export components +if (typeof window !== "undefined") { + window.BRAND = BRAND; + window.BrandLogo = BrandLogo; + window.BrandIcon = BrandIcon; + window.Heading = Heading; + window.Text = Text; + window.Button = Button; + window.Card = Card; + window.ColorSwatch = ColorSwatch; +} +`; +} + +/** + * Generate style guide markdown + */ +function generateStyleGuide(identity: BrandIdentity): string { + const colors = identity.colors || { primary: "#8B5CF6" }; + const typography = identity.typography || {}; + const style = identity.style || {}; + const voice = identity.voice || {}; + + return `# ${identity.name} Brand Style Guide + +${identity.description ? `> ${identity.description}` : ""} + +${identity.tagline ? `**Tagline:** "${identity.tagline}"` : ""} + +--- + +## Brand Identity + +**Name:** ${identity.name} +${identity.industry ? `**Industry:** ${identity.industry}` : ""} +${identity.founded ? `**Founded:** ${identity.founded}` : ""} + +--- + +## Color Palette + +### Primary Colors + +| Color | Hex | Usage | +|-------|-----|-------| +| Primary | \`${colors.primary}\` | Main brand color, CTAs, links | +| Primary Light | \`${lightenColor(colors.primary, 20)}\` | Hover states, backgrounds | +| Primary Dark | \`${darkenColor(colors.primary, 20)}\` | Active states, emphasis | + +${ + colors.secondary + ? `### Secondary Colors + +| Color | Hex | Usage | +|-------|-----|-------| +| Secondary | \`${colors.secondary}\` | Supporting elements | +${colors.accent ? `| Accent | \`${colors.accent}\` | Highlights, notifications |` : ""}` + : "" +} + +### Background & Text + +| Color | Hex | Usage | +|-------|-----|-------| +| Background | \`${colors.background || "#1a1a1a"}\` | Dark backgrounds | +| Text | \`${colors.text || "#1A1A1A"}\` | Primary text | +| Text Muted | \`#6B7280\` | Secondary text | + +${ + colors.palette?.length + ? `### Full Palette + +${colors.palette.map((c) => `- \`${c}\``).join("\n")}` + : "" +} + +--- + +## Typography + +### Font Families + +- **Headings:** ${typography.headingFont || "Inter, system-ui, sans-serif"} +- **Body:** ${typography.bodyFont || "Inter, system-ui, sans-serif"} +${typography.monoFont ? `- **Monospace:** ${typography.monoFont}` : ""} + +### Type Scale + +| Element | Size | Weight | +|---------|------|--------| +| H1 | 3rem (48px) | Bold (700) | +| H2 | 2.25rem (36px) | Bold (700) | +| H3 | 1.875rem (30px) | Semibold (600) | +| H4 | 1.5rem (24px) | Semibold (600) | +| Body | 1rem (16px) | Regular (400) | +| Small | 0.875rem (14px) | Regular (400) | + +--- + +## Logo Usage + +${ + identity.logos?.primary + ? `### Primary Logo + +![${identity.name} Logo](${identity.logos.primary}) + +- Use on light backgrounds +- Maintain clear space equal to the height of the logo +- Minimum size: 100px width` + : `### Logo + +*Logo URL not available. Please provide logo assets.*` +} + +${ + identity.logos?.light + ? `### Light Logo (for dark backgrounds) + +![${identity.name} Light Logo](${identity.logos.light})` + : "" +} + +${ + identity.logos?.dark + ? `### Dark Logo (for light backgrounds) + +![${identity.name} Dark Logo](${identity.logos.dark})` + : "" +} + +${ + identity.logos?.icon + ? `### Icon/Favicon + +![${identity.name} Icon](${identity.logos.icon}) + +- Use for favicons, app icons, social media +- Square format (1:1 ratio)` + : "" +} + +--- + +## Visual Style + +${style.aesthetic ? `**Aesthetic:** ${style.aesthetic}` : ""} +${style.mood ? `**Mood:** ${style.mood}` : ""} +${style.keywords?.length ? `**Keywords:** ${style.keywords.join(", ")}` : ""} + +### UI Elements + +- **Border Radius:** ${style.borderRadius || "0.5rem (8px)"} +- **Shadows:** ${style.shadows || "Subtle, layered shadows"} + +--- + +${ + voice.tone || voice.personality?.length || voice.values?.length + ? `## Brand Voice + +${voice.tone ? `**Tone:** ${voice.tone}` : ""} + +${ + voice.personality?.length + ? `### Personality +${voice.personality.map((p) => `- ${p}`).join("\n")}` + : "" +} + +${ + voice.values?.length + ? `### Core Values +${voice.values.map((v) => `- ${v}`).join("\n")}` + : "" +} + +---` + : "" +} + +## Usage Guidelines + +1. **Consistency:** Always use the exact hex values specified +2. **Contrast:** Ensure sufficient contrast for accessibility (WCAG 2.1 AA) +3. **Spacing:** Use the spacing scale (0.25rem increments) +4. **Typography:** Maintain the type hierarchy +5. **Logo:** Never stretch, distort, or recolor the logo + +--- + +*Generated by Brand MCP* +`; +} + +// Helper functions +function lightenColor(hex: string, percent: number): string { + const num = parseInt(hex.replace("#", ""), 16); + const amt = Math.round(2.55 * percent); + const R = Math.min(255, (num >> 16) + amt); + const G = Math.min(255, ((num >> 8) & 0x00ff) + amt); + const B = Math.min(255, (num & 0x0000ff) + amt); + return `#${((1 << 24) + (R << 16) + (G << 8) + B).toString(16).slice(1)}`; +} + +function darkenColor(hex: string, percent: number): string { + const num = parseInt(hex.replace("#", ""), 16); + const amt = Math.round(2.55 * percent); + const R = Math.max(0, (num >> 16) - amt); + const G = Math.max(0, ((num >> 8) & 0x00ff) - amt); + const B = Math.max(0, (num & 0x0000ff) - amt); + return `#${((1 << 24) + (R << 16) + (G << 8) + B).toString(16).slice(1)}`; +} + +/** + * BRAND_GENERATE - Generate complete design system from brand identity + */ +export const createBrandGenerateTool = (_env: Env) => + createTool({ + id: "BRAND_GENERATE", + _meta: { "ui/resourceUri": "ui://brand-preview" }, + description: `Generate a complete design system from brand identity. + +Creates: +- CSS variables file +- JSX component library +- Markdown style guide + +**Input:** Brand identity object (from BRAND_DISCOVER or manual) +**Output:** Complete design system files ready to use`, + inputSchema: z.object({ + identity: BrandIdentitySchema.describe("Brand identity to generate from"), + outputFormat: z + .enum(["all", "css", "jsx", "styleguide"]) + .optional() + .describe("Which outputs to generate (default: all)"), + }), + outputSchema: z.object({ + brandName: z.string(), + css: z.string().optional().describe("CSS variables file content"), + jsx: z.string().optional().describe("JSX design system content"), + styleGuide: z.string().optional().describe("Markdown style guide"), + }), + execute: async ({ context }) => { + const { identity, outputFormat = "all" } = context; + + const result: { + brandName: string; + css?: string; + jsx?: string; + styleGuide?: string; + } = { + brandName: identity.name, + }; + + if (outputFormat === "all" || outputFormat === "css") { + result.css = generateCSSVariables(identity); + } + + if (outputFormat === "all" || outputFormat === "jsx") { + result.jsx = generateDesignSystemJSX(identity); + } + + if (outputFormat === "all" || outputFormat === "styleguide") { + result.styleGuide = generateStyleGuide(identity); + } + + return result; + }, + }); + +/** + * BRAND_CREATE - Full workflow: discover + generate + */ +export const createBrandCreateTool = (env: Env) => + createTool({ + id: "BRAND_CREATE", + _meta: { "ui/resourceUri": "ui://brand-preview" }, + description: `Complete brand creation workflow. + +This is the main tool - it: +1. Discovers brand identity from website + research +2. Generates complete design system +3. Returns everything ready to use + +**Best for:** One-step brand creation from a website URL.`, + inputSchema: z.object({ + brandName: z.string().describe("Brand or company name"), + websiteUrl: z.string().url().describe("Brand website URL"), + }), + outputSchema: z.object({ + success: z.boolean(), + identity: BrandIdentitySchema.optional(), + css: z.string().optional(), + jsx: z.string().optional(), + styleGuide: z.string().optional(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const { brandName, websiteUrl } = context; + + const firecrawl = env.MESH_REQUEST_CONTEXT?.state?.FIRECRAWL; + const perplexity = env.MESH_REQUEST_CONTEXT?.state?.PERPLEXITY; + + if (!firecrawl && !perplexity) { + return { + success: false, + error: + "No research bindings available. Configure FIRECRAWL and/or PERPLEXITY bindings.", + }; + } + + // Step 1: Discover brand identity (reuse logic from research.ts) + const identity: BrandIdentity = { + name: brandName, + colors: { primary: "#8B5CF6" }, + sources: [], + confidence: "low", + }; + + // Scrape website + if (firecrawl) { + try { + const result = (await firecrawl.firecrawl_scrape({ + url: websiteUrl, + formats: ["branding", "links"], + })) as { branding?: Record }; + + if (result?.branding) { + const branding = result.branding; + + if (branding.colors && typeof branding.colors === "object") { + const colors = branding.colors as Record; + identity.colors = { + primary: colors.primary || colors.main || "#8B5CF6", + secondary: colors.secondary, + accent: colors.accent, + background: colors.background, + text: colors.text, + palette: Object.values(colors).filter( + (c) => typeof c === "string" && c.startsWith("#"), + ), + }; + } + + if (branding.logos) { + if (Array.isArray(branding.logos)) { + identity.logos = { + primary: branding.logos[0] as string, + alternates: branding.logos.slice(1) as string[], + }; + } else if (typeof branding.logos === "object") { + const logos = branding.logos as Record; + identity.logos = { + primary: logos.primary || logos.main, + light: logos.light, + dark: logos.dark, + icon: logos.icon, + }; + } + } + + if (branding.fonts && typeof branding.fonts === "object") { + const fonts = branding.fonts as Record; + identity.typography = { + headingFont: fonts.heading || fonts.title, + bodyFont: fonts.body || fonts.text, + monoFont: fonts.mono, + }; + } + + identity.sources?.push(websiteUrl); + } + } catch (error) { + console.error("Scraping error:", error); + } + } + + // Research with Perplexity + if (perplexity) { + try { + const result = (await perplexity.perplexity_research({ + messages: [ + { + role: "user", + content: `Brief info on ${brandName} (${websiteUrl}): tagline, primary color hex, and brand personality in 2-3 sentences.`, + }, + ], + strip_thinking: true, + })) as { response?: string }; + + if (result?.response) { + const response = result.response; + + // Extract tagline + const taglineMatch = response.match( + /(?:tagline|slogan)[:\s]+["']?([^"'\n.]+)["']?/i, + ); + if (taglineMatch) { + identity.tagline = taglineMatch[1].trim(); + } + + // Extract colors if we don't have good ones + if (identity.colors.primary === "#8B5CF6") { + const hexColors = response.match(/#[0-9A-Fa-f]{6}/g); + if (hexColors?.length) { + identity.colors.primary = hexColors[0]; + } + } + + identity.sources?.push("perplexity-research"); + } + } catch (error) { + console.error("Research error:", error); + } + } + + // Determine confidence + const hasLogo = Boolean(identity.logos?.primary); + const hasColors = identity.colors.primary !== "#8B5CF6"; + identity.confidence = + hasLogo && hasColors ? "high" : hasLogo || hasColors ? "medium" : "low"; + + // Step 2: Generate design system + const css = generateCSSVariables(identity); + const jsx = generateDesignSystemJSX(identity); + const styleGuide = generateStyleGuide(identity); + + return { + success: true, + identity, + css, + jsx, + styleGuide, + }; + }, + }); + +export const generatorTools = [createBrandGenerateTool, createBrandCreateTool]; diff --git a/brand/server/tools/index.ts b/brand/server/tools/index.ts new file mode 100644 index 00000000..aa5ad4d8 --- /dev/null +++ b/brand/server/tools/index.ts @@ -0,0 +1,16 @@ +/** + * Brand MCP Tools + * + * Complete toolset for brand research and design system generation. + */ +import { researchTools } from "./research.ts"; +import { generatorTools } from "./generator.ts"; +import { projectTools } from "./projects.ts"; + +export const tools = [...projectTools, ...researchTools, ...generatorTools]; + +export { researchTools } from "./research.ts"; +export { generatorTools } from "./generator.ts"; +export { projectTools } from "./projects.ts"; +export { BrandIdentitySchema, type BrandIdentity } from "./research.ts"; +export { BrandProjectSchema, type BrandProject } from "./projects.ts"; diff --git a/brand/server/tools/projects.ts b/brand/server/tools/projects.ts new file mode 100644 index 00000000..9e669862 --- /dev/null +++ b/brand/server/tools/projects.ts @@ -0,0 +1,695 @@ +/** + * Brand Project Management Tools + * + * CRUD operations for brand projects with state persistence. + * Projects are saved to filesystem via FILESYSTEM binding for persistence. + * Falls back to in-memory storage if no FS binding is available. + */ +import { createTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../types/env.ts"; +import { BrandIdentitySchema, type BrandIdentity } from "./research.ts"; + +/** + * Project status enum + */ +const ProjectStatusSchema = z.enum([ + "draft", // Just created, no research yet + "researching", // Brand research in progress + "designing", // Design system being generated + "complete", // All done +]); + +export type ProjectStatus = z.infer; + +/** + * Brand project schema + */ +export const BrandProjectSchema = z.object({ + id: z.string().describe("Unique project ID"), + name: z.string().describe("Project name"), + prompt: z.string().optional().describe("Initial prompt/description"), + websiteUrl: z.string().url().optional().describe("Brand website URL"), + status: ProjectStatusSchema.describe("Current project status"), + wizardStep: z + .number() + .default(0) + .describe("Current step in the creation wizard"), + identity: BrandIdentitySchema.optional().describe( + "Discovered brand identity", + ), + css: z.string().optional().describe("Generated CSS variables"), + jsx: z.string().optional().describe("Generated JSX design system"), + styleGuide: z.string().optional().describe("Generated style guide"), + createdAt: z.string().describe("ISO timestamp of creation"), + updatedAt: z.string().describe("ISO timestamp of last update"), +}); + +export type BrandProject = z.infer; + +/** + * In-memory cache for projects (also used as fallback when no FS binding) + */ +const projectCache: Map = new Map(); + +/** + * Projects directory in the filesystem + */ +const PROJECTS_DIR = "brand-projects"; +const INDEX_FILE = `${PROJECTS_DIR}/index.json`; + +/** + * Generate a unique project ID + */ +function generateId(): string { + return `brand_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +} + +/** + * Get the file path for a project + */ +function getProjectPath(projectId: string): string { + return `${PROJECTS_DIR}/${projectId}.json`; +} + +/** + * Helper to save a project to filesystem + * Uses official MCP filesystem API (write_file) + */ +async function saveProject( + fs: Env["MESH_REQUEST_CONTEXT"]["state"]["FILESYSTEM"], + project: BrandProject, +): Promise { + if (!fs) { + // Fallback to in-memory only + projectCache.set(project.id, project); + return; + } + + try { + await fs.write_file({ + path: getProjectPath(project.id), + content: JSON.stringify(project, null, 2), + }); + // Also update cache + projectCache.set(project.id, project); + console.log(`[brand-mcp] 💾 Saved project to FS: ${project.id}`); + } catch (error) { + console.error(`[brand-mcp] Failed to save project to FS:`, error); + // Fallback to in-memory + projectCache.set(project.id, project); + } +} + +/** + * Helper to load a project from filesystem + * Uses official MCP filesystem API (read_file) + */ +async function loadProject( + fs: Env["MESH_REQUEST_CONTEXT"]["state"]["FILESYSTEM"], + projectId: string, +): Promise { + // Check cache first + const cached = projectCache.get(projectId); + if (cached) return cached; + + if (!fs) return null; + + try { + const result = await fs.read_file({ + path: getProjectPath(projectId), + }); + // MCP filesystem returns content in result.content[0].text format + const content = + typeof result === "string" + ? result + : result?.content?.[0]?.text || JSON.stringify(result); + const project = JSON.parse(content) as BrandProject; + projectCache.set(projectId, project); + return project; + } catch { + return null; + } +} + +/** + * Helper to list all projects from filesystem + * Uses official MCP filesystem API (list_directory) + */ +async function listProjects( + fs: Env["MESH_REQUEST_CONTEXT"]["state"]["FILESYSTEM"], +): Promise { + // Return cache if no FS + if (!fs) { + return Array.from(projectCache.values()); + } + + try { + // List directory contents + const result = await fs.list_directory?.({ path: PROJECTS_DIR }); + + if (!result) { + return Array.from(projectCache.values()); + } + + // Parse directory listing to get project files + // MCP filesystem returns content in result.content[0].text format + const listing = + typeof result === "string" + ? result + : result?.content?.[0]?.text || String(result); + const lines = listing.split("\n").filter((l) => l.includes("[FILE]")); + const projects: BrandProject[] = []; + + for (const line of lines) { + const match = line.match(/\[FILE\]\s+(.+\.json)/); + if (match && match[1] !== "index.json") { + const projectId = match[1].replace(".json", ""); + const project = await loadProject(fs, projectId); + if (project) { + projects.push(project); + } + } + } + + // Merge with any cached projects not yet saved + for (const cached of projectCache.values()) { + if (!projects.find((p) => p.id === cached.id)) { + projects.push(cached); + } + } + + return projects; + } catch { + return Array.from(projectCache.values()); + } +} + +/** + * Helper to delete a project from filesystem + * Note: Official MCP filesystem doesn't have delete, so we just remove from cache + * and overwrite the file with empty content if possible + */ +async function deleteProject( + fs: Env["MESH_REQUEST_CONTEXT"]["state"]["FILESYSTEM"], + projectId: string, +): Promise { + projectCache.delete(projectId); + + if (!fs?.write_file) return true; + + try { + // Overwrite with empty marker to "delete" (not ideal but works) + await fs.write_file({ + path: getProjectPath(projectId), + content: '{"deleted":true}', + }); + console.log(`[brand-mcp] 🗑️ Deleted project from FS: ${projectId}`); + return true; + } catch { + return true; // Consider success even if file doesn't exist + } +} + +/** + * PROJECT_CREATE - Create a new brand project + */ +export const createProjectCreateTool = (env: Env) => + createTool({ + id: "PROJECT_CREATE", + _meta: { "ui/resourceUri": "ui://project-wizard" }, + description: `Create a new brand project. + +The project is saved immediately to filesystem and can be resumed later. +Returns the project with its ID for tracking. + +**Use this as the first step** when starting a new brand.`, + inputSchema: z.object({ + name: z.string().describe("Project/brand name"), + prompt: z + .string() + .optional() + .describe("Description or prompt for the brand"), + websiteUrl: z.string().url().optional().describe("Brand website URL"), + }), + outputSchema: z.object({ + success: z.boolean(), + project: BrandProjectSchema.optional(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const { name, prompt, websiteUrl } = context; + + const now = new Date().toISOString(); + const project: BrandProject = { + id: generateId(), + name, + prompt, + websiteUrl, + status: "draft", + wizardStep: 1, // Move to step 1 after creation + createdAt: now, + updatedAt: now, + }; + + const fs = env.MESH_REQUEST_CONTEXT?.state?.FILESYSTEM; + await saveProject(fs, project); + + console.log(`[brand-mcp] 📁 Created project: ${project.id} (${name})`); + + return { + success: true, + project, + }; + }, + }); + +/** + * PROJECT_LIST - List all brand projects + */ +export const createProjectListTool = (env: Env) => + createTool({ + id: "PROJECT_LIST", + _meta: { "ui/resourceUri": "ui://brand-list" }, + description: `List all brand projects. + +Returns projects sorted by last updated (most recent first).`, + inputSchema: z.object({ + status: ProjectStatusSchema.optional().describe("Filter by status"), + }), + outputSchema: z.object({ + projects: z.array(BrandProjectSchema), + total: z.number(), + }), + execute: async ({ context }) => { + const { status } = context; + + const fs = env.MESH_REQUEST_CONTEXT?.state?.FILESYSTEM; + let projects = await listProjects(fs); + + // Filter by status if provided + if (status) { + projects = projects.filter((p) => p.status === status); + } + + // Sort by updatedAt descending + projects.sort( + (a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), + ); + + return { + projects, + total: projects.length, + }; + }, + }); + +/** + * PROJECT_GET - Get a specific project by ID + */ +export const createProjectGetTool = (env: Env) => + createTool({ + id: "PROJECT_GET", + _meta: { "ui/resourceUri": "ui://project-wizard" }, + description: `Get a brand project by ID. + +Use this to resume a project from where you left off.`, + inputSchema: z.object({ + projectId: z.string().describe("Project ID to retrieve"), + }), + outputSchema: z.object({ + success: z.boolean(), + project: BrandProjectSchema.optional(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const { projectId } = context; + + const fs = env.MESH_REQUEST_CONTEXT?.state?.FILESYSTEM; + const project = await loadProject(fs, projectId); + + if (!project) { + return { + success: false, + error: `Project not found: ${projectId}`, + }; + } + + return { + success: true, + project, + }; + }, + }); + +/** + * PROJECT_UPDATE - Update a project's state + */ +export const createProjectUpdateTool = (env: Env) => + createTool({ + id: "PROJECT_UPDATE", + _meta: { "ui/resourceUri": "ui://project-wizard" }, + description: `Update a brand project. + +Use this to save progress at any step of the wizard.`, + inputSchema: z.object({ + projectId: z.string().describe("Project ID to update"), + name: z.string().optional().describe("Update project name"), + prompt: z.string().optional().describe("Update prompt"), + websiteUrl: z.string().url().optional().describe("Update website URL"), + status: ProjectStatusSchema.optional().describe("Update status"), + wizardStep: z.number().optional().describe("Update wizard step"), + identity: BrandIdentitySchema.optional().describe("Set brand identity"), + css: z.string().optional().describe("Set generated CSS"), + jsx: z.string().optional().describe("Set generated JSX"), + styleGuide: z.string().optional().describe("Set generated style guide"), + }), + outputSchema: z.object({ + success: z.boolean(), + project: BrandProjectSchema.optional(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const { projectId, ...updates } = context; + + const fs = env.MESH_REQUEST_CONTEXT?.state?.FILESYSTEM; + const project = await loadProject(fs, projectId); + + if (!project) { + return { + success: false, + error: `Project not found: ${projectId}`, + }; + } + + // Apply updates + const updatedProject: BrandProject = { + ...project, + ...Object.fromEntries( + Object.entries(updates).filter(([_, v]) => v !== undefined), + ), + updatedAt: new Date().toISOString(), + }; + + await saveProject(fs, updatedProject); + + console.log( + `[brand-mcp] 📝 Updated project: ${projectId} (step ${updatedProject.wizardStep}, status: ${updatedProject.status})`, + ); + + return { + success: true, + project: updatedProject, + }; + }, + }); + +/** + * PROJECT_DELETE - Delete a project + */ +export const createProjectDeleteTool = (env: Env) => + createTool({ + id: "PROJECT_DELETE", + description: `Delete a brand project. + +This action cannot be undone.`, + inputSchema: z.object({ + projectId: z.string().describe("Project ID to delete"), + }), + outputSchema: z.object({ + success: z.boolean(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const { projectId } = context; + + const fs = env.MESH_REQUEST_CONTEXT?.state?.FILESYSTEM; + const project = await loadProject(fs, projectId); + + if (!project) { + return { + success: false, + error: `Project not found: ${projectId}`, + }; + } + + await deleteProject(fs, projectId); + + console.log(`[brand-mcp] 🗑️ Deleted project: ${projectId}`); + + return { + success: true, + }; + }, + }); + +/** + * PROJECT_RESEARCH - Research brand for a project + */ +export const createProjectResearchTool = (env: Env) => + createTool({ + id: "PROJECT_RESEARCH", + _meta: { "ui/resourceUri": "ui://project-wizard" }, + description: `Research and discover brand identity for a project. + +This combines scraping and AI research, then saves the results to the project.`, + inputSchema: z.object({ + projectId: z.string().describe("Project ID"), + websiteUrl: z.string().url().optional().describe("Website to research"), + }), + outputSchema: z.object({ + success: z.boolean(), + project: BrandProjectSchema.optional(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const { projectId, websiteUrl } = context; + + const fs = env.MESH_REQUEST_CONTEXT?.state?.FILESYSTEM; + const project = await loadProject(fs, projectId); + + if (!project) { + return { + success: false, + error: `Project not found: ${projectId}`, + }; + } + + const url = websiteUrl || project.websiteUrl; + if (!url) { + return { + success: false, + error: "No website URL provided for research", + }; + } + + // Update status to researching + project.status = "researching"; + project.websiteUrl = url; + project.updatedAt = new Date().toISOString(); + await saveProject(fs, project); + + const firecrawl = env.MESH_REQUEST_CONTEXT?.state?.FIRECRAWL; + const perplexity = env.MESH_REQUEST_CONTEXT?.state?.PERPLEXITY; + + const identity: Partial = { + name: project.name, + sources: [], + confidence: "low", + }; + + // Scrape with Content Scraper + if (firecrawl) { + try { + const result = (await firecrawl.firecrawl_scrape({ + url, + formats: ["branding", "links"], + })) as { branding?: Record }; + + if (result?.branding) { + const branding = result.branding; + + if (branding.colors && typeof branding.colors === "object") { + const colors = branding.colors as Record; + identity.colors = { + primary: colors.primary || colors.main || "#8B5CF6", + secondary: colors.secondary, + accent: colors.accent, + background: colors.background, + text: colors.text, + }; + } + + if (branding.logos) { + if (Array.isArray(branding.logos)) { + identity.logos = { primary: branding.logos[0] as string }; + } else if (typeof branding.logos === "object") { + const logos = branding.logos as Record; + identity.logos = { + primary: logos.primary || logos.main, + light: logos.light, + dark: logos.dark, + icon: logos.icon, + }; + } + } + + if (branding.fonts && typeof branding.fonts === "object") { + const fonts = branding.fonts as Record; + identity.typography = { + headingFont: fonts.heading || fonts.title, + bodyFont: fonts.body || fonts.text, + monoFont: fonts.mono, + }; + } + + identity.sources?.push(url); + } + } catch (error) { + console.error("[brand-mcp] Scraping error:", error); + } + } + + // Research with Perplexity + if (perplexity) { + try { + const result = (await perplexity.perplexity_research({ + messages: [ + { + role: "user", + content: `Brief info on ${project.name} (${url}): tagline, primary color hex, and brand personality in 2-3 sentences.`, + }, + ], + strip_thinking: true, + })) as { response?: string }; + + if (result?.response) { + const response = result.response; + + // Extract tagline + const taglineMatch = response.match( + /(?:tagline|slogan)[:\s]+["']?([^"'\n.]+)["']?/i, + ); + if (taglineMatch) { + identity.tagline = taglineMatch[1].trim(); + } + + // Extract colors if we don't have good ones + if (!identity.colors || identity.colors.primary === "#8B5CF6") { + const hexColors = response.match(/#[0-9A-Fa-f]{6}/g); + if (hexColors?.length) { + identity.colors = { + ...(identity.colors || {}), + primary: hexColors[0], + }; + } + } + + identity.sources?.push("perplexity-research"); + } + } catch (error) { + console.error("[brand-mcp] Research error:", error); + } + } + + // Set default colors if none found + if (!identity.colors) { + identity.colors = { primary: "#8B5CF6" }; + } + + // Determine confidence + const hasLogo = Boolean(identity.logos?.primary); + const hasColors = identity.colors.primary !== "#8B5CF6"; + identity.confidence = + hasLogo && hasColors ? "high" : hasLogo || hasColors ? "medium" : "low"; + + // Update project with identity + project.identity = identity as BrandIdentity; + project.status = "designing"; + project.wizardStep = 2; + project.updatedAt = new Date().toISOString(); + await saveProject(fs, project); + + console.log( + `[brand-mcp] 🔍 Researched project: ${projectId} (confidence: ${identity.confidence})`, + ); + + return { + success: true, + project, + }; + }, + }); + +/** + * PROJECT_GENERATE - Generate design system for a project + */ +export const createProjectGenerateTool = (env: Env) => + createTool({ + id: "PROJECT_GENERATE", + _meta: { "ui/resourceUri": "ui://brand-preview" }, + description: `Generate design system files for a project. + +Creates CSS, JSX, and style guide from the project's brand identity.`, + inputSchema: z.object({ + projectId: z.string().describe("Project ID"), + }), + outputSchema: z.object({ + success: z.boolean(), + project: BrandProjectSchema.optional(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const { projectId } = context; + + const fs = env.MESH_REQUEST_CONTEXT?.state?.FILESYSTEM; + const project = await loadProject(fs, projectId); + + if (!project) { + return { + success: false, + error: `Project not found: ${projectId}`, + }; + } + + if (!project.identity) { + return { + success: false, + error: "Project has no brand identity. Run research first.", + }; + } + + // Import generator functions + const { + generateCSSVariables, + generateDesignSystemJSX, + generateStyleGuide, + } = await import("./generator-utils.ts"); + + project.css = generateCSSVariables(project.identity); + project.jsx = generateDesignSystemJSX(project.identity); + project.styleGuide = generateStyleGuide(project.identity); + project.status = "complete"; + project.wizardStep = 3; + project.updatedAt = new Date().toISOString(); + await saveProject(fs, project); + + console.log(`[brand-mcp] ✨ Generated design system for: ${projectId}`); + + return { + success: true, + project, + }; + }, + }); + +export const projectTools = [ + createProjectCreateTool, + createProjectListTool, + createProjectGetTool, + createProjectUpdateTool, + createProjectDeleteTool, + createProjectResearchTool, + createProjectGenerateTool, +]; diff --git a/brand/server/tools/research.ts b/brand/server/tools/research.ts new file mode 100644 index 00000000..ec2514ae --- /dev/null +++ b/brand/server/tools/research.ts @@ -0,0 +1,663 @@ +/** + * Brand Research Tools + * + * Uses Firecrawl and Perplexity to research and extract brand identity. + */ +import { createTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../types/env.ts"; + +/** + * Brand identity schema + */ +export const BrandIdentitySchema = z.object({ + name: z.string().describe("Official brand name"), + tagline: z.string().optional().describe("Brand tagline or slogan"), + description: z.string().optional().describe("Brief brand description"), + industry: z.string().optional().describe("Industry or sector"), + founded: z.string().optional().describe("Year founded"), + headquarters: z.string().optional().describe("Company headquarters"), + + colors: z + .object({ + primary: z.string().describe("Primary brand color (hex)"), + secondary: z.string().optional().describe("Secondary brand color (hex)"), + accent: z.string().optional().describe("Accent color (hex)"), + background: z.string().optional().describe("Background color (hex)"), + text: z.string().optional().describe("Primary text color (hex)"), + palette: z + .array(z.string()) + .optional() + .describe("Full color palette (hex values)"), + }) + .describe("Brand color palette"), + + logos: z + .object({ + primary: z.string().optional().describe("Primary logo URL"), + light: z.string().optional().describe("Light logo for dark backgrounds"), + dark: z.string().optional().describe("Dark logo for light backgrounds"), + icon: z.string().optional().describe("Square icon/favicon URL"), + alternates: z + .array(z.string()) + .optional() + .describe("Other logo variants"), + }) + .optional() + .describe("Logo image URLs"), + + typography: z + .object({ + headingFont: z.string().optional().describe("Primary heading font"), + bodyFont: z.string().optional().describe("Body text font"), + monoFont: z.string().optional().describe("Monospace font"), + fontWeights: z.array(z.string()).optional().describe("Common weights"), + letterSpacing: z.string().optional().describe("Letter spacing style"), + }) + .optional() + .describe("Typography information"), + + style: z + .object({ + aesthetic: z.string().optional().describe("Visual aesthetic"), + mood: z.string().optional().describe("Brand mood/tone"), + keywords: z.array(z.string()).optional().describe("Style keywords"), + borderRadius: z.string().optional().describe("Border radius style"), + shadows: z.string().optional().describe("Shadow style"), + }) + .optional() + .describe("Visual style attributes"), + + voice: z + .object({ + tone: z.string().optional().describe("Communication tone"), + personality: z + .array(z.string()) + .optional() + .describe("Brand personality traits"), + values: z.array(z.string()).optional().describe("Core values"), + }) + .optional() + .describe("Brand voice and personality"), + + sources: z.array(z.string()).optional().describe("Research sources"), + confidence: z.enum(["high", "medium", "low"]).describe("Confidence level"), + rawData: z.unknown().optional().describe("Raw research data"), +}); + +export type BrandIdentity = z.infer; + +/** + * BRAND_SCRAPE - Extract brand identity from a website using Firecrawl + */ +export const createBrandScrapeTool = (env: Env) => + createTool({ + id: "BRAND_SCRAPE", + description: `Scrape a website to extract brand identity using Firecrawl. + +Uses Firecrawl's 'branding' format to extract: +- Colors (primary, secondary, accent, background) +- Typography (fonts, weights, spacing) +- Logo images found on the page +- Visual style (aesthetic, shadows, border radius) + +**Requires:** FIRECRAWL binding to be configured. + +**Best for:** When you have the brand's website URL and want to extract their actual design system.`, + inputSchema: z.object({ + url: z.string().url().describe("Website URL to scrape"), + includeScreenshot: z + .boolean() + .optional() + .describe("Also capture a screenshot"), + }), + outputSchema: z.object({ + success: z.boolean(), + identity: BrandIdentitySchema.optional(), + screenshot: z.string().optional().describe("Screenshot URL if requested"), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const { url, includeScreenshot } = context; + + const firecrawl = env.MESH_REQUEST_CONTEXT?.state?.FIRECRAWL; + if (!firecrawl) { + return { + success: false, + error: + "FIRECRAWL binding not configured. Add the Firecrawl binding to enable website scraping.", + }; + } + + try { + const formats: string[] = ["branding", "links"]; + if (includeScreenshot) { + formats.push("screenshot"); + } + + const result = (await firecrawl.firecrawl_scrape({ + url, + formats, + })) as { + branding?: { + colors?: Record; + fonts?: Record; + typography?: Record; + logos?: Record | string[]; + style?: Record; + }; + links?: string[]; + screenshot?: string; + }; + + if (!result?.branding) { + return { + success: false, + error: "No branding data extracted from the page", + }; + } + + const branding = result.branding; + + // Parse colors + const colors: BrandIdentity["colors"] = { + primary: + branding.colors?.primary || branding.colors?.main || "#000000", + secondary: branding.colors?.secondary, + accent: branding.colors?.accent, + background: branding.colors?.background || branding.colors?.bg, + text: branding.colors?.text, + palette: Object.values(branding.colors || {}).filter( + (c): c is string => typeof c === "string" && c.startsWith("#"), + ), + }; + + // Parse logos + let logos: BrandIdentity["logos"]; + if (branding.logos) { + if (Array.isArray(branding.logos)) { + logos = { + primary: branding.logos[0], + alternates: branding.logos.slice(1), + }; + } else { + logos = { + primary: branding.logos.primary || branding.logos.main, + light: branding.logos.light || branding.logos.white, + dark: branding.logos.dark || branding.logos.black, + icon: branding.logos.icon || branding.logos.favicon, + }; + } + } + + // Parse typography + const typography: BrandIdentity["typography"] = { + headingFont: + branding.fonts?.heading || + branding.fonts?.title || + (branding.typography?.headingFont as string), + bodyFont: + branding.fonts?.body || + branding.fonts?.text || + (branding.typography?.bodyFont as string), + monoFont: branding.fonts?.mono || branding.fonts?.code, + }; + + // Parse style + const style: BrandIdentity["style"] = { + borderRadius: branding.style?.borderRadius as string, + shadows: branding.style?.shadows as string, + }; + + // Extract brand name from URL + const urlObj = new URL(url); + const brandName = + urlObj.hostname.replace("www.", "").split(".")[0] || "Unknown Brand"; + + const identity: BrandIdentity = { + name: brandName.charAt(0).toUpperCase() + brandName.slice(1), + colors, + logos, + typography, + style, + sources: [url], + confidence: + logos?.primary && colors.primary !== "#000000" ? "high" : "medium", + }; + + return { + success: true, + identity, + screenshot: result.screenshot, + }; + } catch (error) { + return { + success: false, + error: `Scraping failed: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BRAND_RESEARCH - Deep research on a brand using Perplexity + */ +export const createBrandResearchTool = (env: Env) => + createTool({ + id: "BRAND_RESEARCH", + description: `Research a brand using Perplexity AI to gather comprehensive information. + +Discovers: +- Brand history and background +- Color palette and visual identity +- Logo variations and assets +- Typography and design guidelines +- Brand voice and personality +- Industry positioning + +**Requires:** PERPLEXITY binding to be configured. + +**Best for:** When you need comprehensive brand information beyond what's visible on their website.`, + inputSchema: z.object({ + brandName: z.string().describe("Brand or company name to research"), + websiteUrl: z.string().url().optional().describe("Brand website URL"), + focusAreas: z + .array(z.enum(["visual", "voice", "history", "guidelines", "assets"])) + .optional() + .describe("Specific areas to focus research on"), + }), + outputSchema: z.object({ + success: z.boolean(), + identity: BrandIdentitySchema.optional(), + research: z.string().optional().describe("Full research text"), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const { brandName, websiteUrl, focusAreas } = context; + + const perplexity = env.MESH_REQUEST_CONTEXT?.state?.PERPLEXITY; + if (!perplexity) { + return { + success: false, + error: + "PERPLEXITY binding not configured. Add the Perplexity binding to enable brand research.", + }; + } + + try { + const focusText = focusAreas?.length + ? `Focus especially on: ${focusAreas.join(", ")}.` + : ""; + + const websiteText = websiteUrl + ? ` Their website is ${websiteUrl}.` + : ""; + + const result = (await perplexity.perplexity_research({ + messages: [ + { + role: "system", + content: `You are a brand research expert. Extract comprehensive brand identity information in a structured way. Include specific hex color codes, font names, and URLs when found.`, + }, + { + role: "user", + content: `Research the brand "${brandName}".${websiteText} + +I need comprehensive information about: + +1. **Brand Identity** + - Official brand name and tagline + - Industry and market position + - Company history and founding + +2. **Visual Identity** + - Primary, secondary, and accent colors (with hex codes if available) + - Logo variations and where to find them + - Typography (heading fonts, body fonts) + - Overall visual style and aesthetic + +3. **Brand Voice** + - Tone of communication + - Key brand values + - Personality traits + +4. **Design Guidelines** + - Any public brand guidelines or press kits + - Logo usage rules + - Color usage guidelines + +${focusText} + +Please provide specific details like hex color codes (#RRGGBB), font names, and URLs to logo assets when you can find them.`, + }, + ], + strip_thinking: true, + })) as { response?: string }; + + if (!result?.response) { + return { + success: false, + error: "No research response received", + }; + } + + // Parse the research response to extract structured data + const response = result.response; + + // Try to extract colors from the response + const hexColors = response.match(/#[0-9A-Fa-f]{6}/g) || []; + const colors: BrandIdentity["colors"] = { + primary: hexColors[0] || "#000000", + secondary: hexColors[1], + accent: hexColors[2], + palette: [...new Set(hexColors)], + }; + + // Try to extract logo URLs + const logoUrls = + response.match(/https?:\/\/[^\s<>"]+\.(png|svg|jpg|jpeg|webp)/gi) || + []; + const logos: BrandIdentity["logos"] = logoUrls.length + ? { + primary: logoUrls[0], + alternates: logoUrls.slice(1), + } + : undefined; + + // Extract description (first paragraph-like content) + const descMatch = response.match(/is\s+(?:a|an)\s+([^.]+\.)/i); + const description = descMatch ? descMatch[0] : undefined; + + const identity: BrandIdentity = { + name: brandName, + description, + colors, + logos, + sources: ["perplexity-research"], + confidence: hexColors.length > 0 ? "medium" : "low", + rawData: { research: response }, + }; + + return { + success: true, + identity, + research: response, + }; + } catch (error) { + return { + success: false, + error: `Research failed: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }, + }); + +/** + * BRAND_DISCOVER - Combined scraping and research for complete brand identity + */ +export const createBrandDiscoverTool = (env: Env) => + createTool({ + id: "BRAND_DISCOVER", + description: `Comprehensive brand discovery combining web scraping and AI research. + +This is the most complete tool - it: +1. Scrapes the brand website for visual identity (if FIRECRAWL available) +2. Researches the brand using AI (if PERPLEXITY available) +3. Combines results into a complete brand identity + +**Best for:** Creating a complete brand profile with maximum information.`, + inputSchema: z.object({ + brandName: z.string().describe("Brand or company name"), + websiteUrl: z.string().url().describe("Brand website URL"), + }), + outputSchema: z.object({ + success: z.boolean(), + identity: BrandIdentitySchema.optional(), + scrapeData: z.unknown().optional(), + researchData: z.unknown().optional(), + error: z.string().optional(), + }), + execute: async ({ context }) => { + const { brandName, websiteUrl } = context; + + const firecrawl = env.MESH_REQUEST_CONTEXT?.state?.FIRECRAWL; + const perplexity = env.MESH_REQUEST_CONTEXT?.state?.PERPLEXITY; + + if (!firecrawl && !perplexity) { + return { + success: false, + error: + "No research bindings available. Configure FIRECRAWL and/or PERPLEXITY bindings.", + }; + } + + const identity: Partial = { + name: brandName, + sources: [], + confidence: "low", + }; + + let scrapeData: unknown; + let researchData: unknown; + + // Step 1: Scrape the website + if (firecrawl) { + try { + const scrapeResult = (await firecrawl.firecrawl_scrape({ + url: websiteUrl, + formats: ["branding", "links"], + })) as { branding?: Record; links?: string[] }; + + if (scrapeResult?.branding) { + scrapeData = scrapeResult.branding; + const branding = scrapeResult.branding; + + // Extract colors + if (branding.colors && typeof branding.colors === "object") { + const colors = branding.colors as Record; + identity.colors = { + primary: colors.primary || colors.main || "#000000", + secondary: colors.secondary, + accent: colors.accent, + background: colors.background, + text: colors.text, + palette: Object.values(colors).filter( + (c) => typeof c === "string" && c.startsWith("#"), + ), + }; + } + + // Extract logos + if (branding.logos) { + if (Array.isArray(branding.logos)) { + identity.logos = { + primary: branding.logos[0] as string, + alternates: branding.logos.slice(1) as string[], + }; + } else if (typeof branding.logos === "object") { + const logos = branding.logos as Record; + identity.logos = { + primary: logos.primary || logos.main, + light: logos.light, + dark: logos.dark, + icon: logos.icon, + }; + } + } + + // Extract typography + if (branding.fonts && typeof branding.fonts === "object") { + const fonts = branding.fonts as Record; + identity.typography = { + headingFont: fonts.heading || fonts.title, + bodyFont: fonts.body || fonts.text, + monoFont: fonts.mono, + }; + } + + identity.sources?.push(websiteUrl); + identity.confidence = identity.logos?.primary ? "high" : "medium"; + } + } catch (error) { + console.error("Scraping error:", error); + } + } + + // Step 2: Research the brand + if (perplexity) { + try { + const researchResult = (await perplexity.perplexity_research({ + messages: [ + { + role: "system", + content: + "You are a brand research expert. Provide concise, factual information about the brand.", + }, + { + role: "user", + content: `Tell me about ${brandName} (${websiteUrl}). Include: +1. Tagline or slogan +2. Industry and founding year +3. Brand colors (hex codes if known) +4. Brand personality and values +5. Any known brand guidelines or press kit URLs`, + }, + ], + strip_thinking: true, + })) as { response?: string }; + + if (researchResult?.response) { + researchData = researchResult.response; + const response = researchResult.response; + + // Extract tagline + const taglineMatch = response.match( + /(?:tagline|slogan)[:\s]+["']?([^"'\n.]+)["']?/i, + ); + if (taglineMatch) { + identity.tagline = taglineMatch[1].trim(); + } + + // Extract description + if (!identity.description) { + const descMatch = response.match(/is\s+(?:a|an)\s+([^.]+\.)/i); + if (descMatch) { + identity.description = descMatch[0]; + } + } + + // Extract any colors we missed + if ( + !identity.colors?.primary || + identity.colors.primary === "#000000" + ) { + const hexColors = response.match(/#[0-9A-Fa-f]{6}/g); + if (hexColors?.length) { + identity.colors = { + ...(identity.colors || {}), + primary: hexColors[0], + palette: [...new Set(hexColors)], + }; + } + } + + // Extract industry + const industryMatch = response.match( + /(?:industry|sector|space)[:\s]+([^.]+)/i, + ); + if (industryMatch) { + identity.industry = industryMatch[1].trim(); + } + + identity.sources?.push("perplexity-research"); + } + } catch (error) { + console.error("Research error:", error); + } + } + + // Determine final confidence + const hasLogo = Boolean(identity.logos?.primary); + const hasColors = Boolean( + identity.colors?.primary && identity.colors.primary !== "#000000", + ); + identity.confidence = + hasLogo && hasColors ? "high" : hasLogo || hasColors ? "medium" : "low"; + + return { + success: true, + identity: identity as BrandIdentity, + scrapeData, + researchData, + }; + }, + }); + +/** + * BRAND_STATUS - Check available research capabilities + */ +export const createBrandStatusTool = (env: Env) => + createTool({ + id: "BRAND_STATUS", + description: `Check which brand research capabilities are available. + +Returns the status of FIRECRAWL and PERPLEXITY bindings and what each enables.`, + inputSchema: z.object({}), + outputSchema: z.object({ + firecrawl: z.object({ + available: z.boolean(), + capabilities: z.array(z.string()), + }), + perplexity: z.object({ + available: z.boolean(), + capabilities: z.array(z.string()), + }), + recommendation: z.string(), + }), + execute: async () => { + const firecrawl = env.MESH_REQUEST_CONTEXT?.state?.FIRECRAWL; + const perplexity = env.MESH_REQUEST_CONTEXT?.state?.PERPLEXITY; + + return { + firecrawl: { + available: Boolean(firecrawl), + capabilities: firecrawl + ? [ + "Extract colors from website CSS", + "Identify typography and fonts", + "Find logo images", + "Capture visual style", + "Take screenshots", + ] + : [], + }, + perplexity: { + available: Boolean(perplexity), + capabilities: perplexity + ? [ + "Research brand history", + "Find brand guidelines", + "Discover logo URLs", + "Analyze brand voice", + "Find color palettes", + ] + : [], + }, + recommendation: + firecrawl && perplexity + ? "Full capabilities available! Use BRAND_DISCOVER for complete brand profiles." + : firecrawl + ? "Firecrawl available for website scraping. Add Perplexity for deeper research." + : perplexity + ? "Perplexity available for research. Add Firecrawl for direct website extraction." + : "No bindings configured. Add FIRECRAWL and/or PERPLEXITY bindings.", + }; + }, + }); + +export const researchTools = [ + createBrandScrapeTool, + createBrandResearchTool, + createBrandDiscoverTool, + createBrandStatusTool, +]; diff --git a/brand/server/types/env.ts b/brand/server/types/env.ts new file mode 100644 index 00000000..d0f12ca4 --- /dev/null +++ b/brand/server/types/env.ts @@ -0,0 +1,35 @@ +/** + * Environment Type Definitions for Brand MCP + */ +import { + BindingOf, + type DefaultEnv, + type BindingRegistry, +} from "@decocms/runtime"; +import { z } from "zod"; + +export const StateSchema = z.object({ + PERPLEXITY: BindingOf("@deco/perplexity-ai") + .optional() + .describe( + "Perplexity AI for brand research - searches for logos, colors, brand identity", + ), + FIRECRAWL: BindingOf("@deco/firecrawl") + .optional() + .describe( + "Firecrawl for web scraping - extracts brand assets from websites", + ), + // Use mcp-filesystem binding for compatibility with both: + // - @modelcontextprotocol/server-filesystem (official) + // - @decocms/mcp-local-fs (our implementation) + FILESYSTEM: BindingOf("@deco/mcp-filesystem") + .optional() + .describe( + "Filesystem for persistent project storage - works with official MCP filesystem or local-fs", + ), +}); + +export type Env = DefaultEnv; + +// Re-export Registry type for use in main.ts +export type { Registry } from "../../../shared/registry.ts"; diff --git a/brand/tsconfig.json b/brand/tsconfig.json new file mode 100644 index 00000000..fc56183c --- /dev/null +++ b/brand/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "resolveJsonModule": true, + "isolatedModules": true, + "types": ["bun-types"] + }, + "include": ["server/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/bun.lock b/bun.lock index 3f803bb5..9929e27c 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "@decocms/mcps", @@ -28,6 +27,20 @@ "typescript": "^5.7.2", }, }, + "blog": { + "name": "@decocms/blog", + "version": "1.0.0", + "dependencies": { + "@decocms/bindings": "^1.0.9", + "@decocms/runtime": "1.2.0", + "zod": "^4.0.0", + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "1.25.1", + "deco-cli": "^0.28.0", + "typescript": "^5.7.2", + }, + }, "blog-post-generator": { "name": "blog-post-generator", "version": "1.0.0", @@ -41,6 +54,34 @@ "typescript": "^5.7.2", }, }, + "bookmarks": { + "name": "@decocms/bookmarks", + "version": "1.0.0", + "dependencies": { + "@decocms/bindings": "^1.0.9", + "@decocms/runtime": "1.2.0", + "zod": "^4.0.0", + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "1.25.1", + "deco-cli": "^0.28.0", + "typescript": "^5.7.2", + }, + }, + "brand": { + "name": "@decocms/brand", + "version": "1.0.0", + "dependencies": { + "@decocms/bindings": "^1.0.9", + "@decocms/runtime": "1.2.0", + "zod": "^4.0.0", + }, + "devDependencies": { + "@decocms/mcps-shared": "workspace:*", + "@modelcontextprotocol/sdk": "1.25.1", + "typescript": "^5.7.2", + }, + }, "content-scraper": { "name": "content-scraper", "version": "1.0.0", @@ -193,21 +234,6 @@ "typescript": "^5.7.2", }, }, - "google-apps-script": { - "name": "google-apps-script", - "version": "1.0.0", - "dependencies": { - "@decocms/runtime": "1.2.5", - "zod": "^4.0.0", - }, - "devDependencies": { - "@decocms/mcps-shared": "workspace:*", - "@modelcontextprotocol/sdk": "1.25.1", - "bun-types": "^1.3.7", - "deco-cli": "^0.28.0", - "typescript": "^5.7.2", - }, - }, "google-big-query": { "name": "google-big-query", "version": "1.0.0", @@ -387,6 +413,22 @@ "typescript": "^5.7.2", }, }, + "local-fs": { + "name": "@decocms/mcp-local-fs", + "version": "1.1.0", + "bin": { + "mcp-local-fs": "./dist/cli.js", + "local-fs-serve": "./server/serve.ts", + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.20.2", + "zod": "^3.24.0", + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + }, + }, "mcp-studio": { "name": "mcp-studio", "version": "1.0.0", @@ -640,6 +682,22 @@ "typescript": "^5.7.2", }, }, + "slides": { + "name": "@decocms/slides", + "version": "1.0.0", + "dependencies": { + "@decocms/bindings": "^1.0.9", + "@decocms/runtime": "1.2.0", + "zod": "^4.0.0", + }, + "devDependencies": { + "@decocms/mcps-shared": "1.0.0", + "@mastra/core": "^0.24.0", + "@modelcontextprotocol/sdk": "1.25.1", + "deco-cli": "^0.28.0", + "typescript": "^5.7.2", + }, + }, "sora": { "name": "sora", "version": "1.0.0", @@ -756,28 +814,6 @@ "wrangler": "^4.28.0", }, }, - "vtex-docs": { - "name": "vtex-docs", - "version": "1.0.0", - "dependencies": { - "@ai-sdk/openai": "^1.3.22", - "@ai-sdk/openai-compatible": "^0.2.14", - "@decocms/runtime": "1.1.3", - "@supabase/supabase-js": "^2.49.1", - "ai": "^4.3.16", - "zod": "^4.0.0", - }, - "devDependencies": { - "@cloudflare/workers-types": "^4.20260128.0", - "@decocms/mcps-shared": "1.0.0", - "@langchain/textsplitters": "^0.1.0", - "@mastra/core": "^0.24.0", - "@modelcontextprotocol/sdk": "1.20.2", - "@types/mime-db": "^1.43.6", - "deco-cli": "^0.28.0", - "typescript": "^5.7.2", - }, - }, "whatsapp": { "name": "whatsappagent", "version": "1.0.0", @@ -795,21 +831,6 @@ "typescript": "5.9.3", }, }, - "whatsapp-management": { - "name": "whatsapp-management", - "version": "1.0.0", - "dependencies": { - "@decocms/bindings": "^1.1.3", - "@decocms/runtime": "1.1.3", - "hono": "^4.11.3", - "zod": "^4.0.0", - }, - "devDependencies": { - "@decocms/mcps-shared": "1.0.0", - "deco-cli": "0.28.5", - "typescript": "5.9.3", - }, - }, "whisper": { "name": "whisper", "version": "1.0.0", @@ -842,9 +863,7 @@ "@ai-sdk/mistral-v5": ["@ai-sdk/mistral@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.16" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-np2bTlL5ZDi7iAOPCF5SZ5xKqls059iOvsigbgd9VNUCIrWSf6GYOaPvoWEgJ650TUOZitTfMo9MiEhLgutPfA=="], - "@ai-sdk/openai": ["@ai-sdk/openai@1.3.24", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-GYXnGJTHRTZc4gJMSmFRgEQudjqd4PUN0ZjQhPwOAYH1yOAvQoG/Ikqs+HyISRbLPCrhbZnPKCNHuRU4OfpW0Q=="], - - "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@0.2.16", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-LkvfcM8slJedRyJa/MiMiaOzcMjV1zNDwzTHEGz7aAsgsQV0maLfmJRi/nuSwf5jmp0EouC+JXXDUj2l94HgQw=="], + "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.12" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Q+lwBIeMprc/iM+vg1yGjvzRrp74l316wDpqWdbmd4VXXlllblzGsUgBLTeKvcEapFTgqk0FRETvSb58Y6dsfA=="], "@ai-sdk/openai-compatible-v5": ["@ai-sdk/openai-compatible@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.12" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Q+lwBIeMprc/iM+vg1yGjvzRrp74l316wDpqWdbmd4VXXlllblzGsUgBLTeKvcEapFTgqk0FRETvSb58Y6dsfA=="], @@ -896,7 +915,7 @@ "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.2", "", { "dependencies": { "@aws-sdk/core": "^3.973.2", "@aws-sdk/nested-clients": "3.975.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-mlaw2aiI3DrimW85ZMn3g7qrtHueidS58IGytZ+mbFpsYLK5wMjCAKZQtt7VatLMtSBG/dn/EY4njbnYXIDKeQ=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.2", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.2", "@aws-sdk/credential-provider-http": "^3.972.3", "@aws-sdk/credential-provider-ini": "^3.972.2", "@aws-sdk/credential-provider-process": "^3.972.2", "@aws-sdk/credential-provider-sso": "^3.972.2", "@aws-sdk/credential-provider-web-identity": "^3.972.2", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-Lz1J5IZdTjLYTVIcDP5DVDgi1xlgsF3p1cnvmbfKbjCRhQpftN2e2J4NFfRRvPD54W9+bZ8l5VipPXtTYK7aEg=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.3", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.2", "@aws-sdk/credential-provider-http": "^3.972.4", "@aws-sdk/credential-provider-ini": "^3.972.2", "@aws-sdk/credential-provider-process": "^3.972.2", "@aws-sdk/credential-provider-sso": "^3.972.2", "@aws-sdk/credential-provider-web-identity": "^3.972.2", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-iu+JwWHM7tHowKqE+8wNmI3sM6mPEiI9Egscz2BEV7adyKmV95oR9tBO4VIOl72FGDi7X9mXg19VtqIpSkEEsA=="], "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.2", "", { "dependencies": { "@aws-sdk/core": "^3.973.2", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-NLKLTT7jnUe9GpQAVkPTJO+cs2FjlQDt5fArIYS7h/Iw/CvamzgGYGFRVD2SE05nOHCMwafUSi42If8esGFV+g=="], @@ -990,8 +1009,6 @@ "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], - "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], - "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.12.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20260115.0" }, "optionalPeers": ["workerd"] }, "sha512-NK4vN+2Z/GbfGS4BamtbbVk1rcu5RmqaYGiyHJQrA09AoxdZPHDF3W/EhgI0YSK8p3vRo/VNCtbSJFPON7FWMQ=="], @@ -1016,13 +1033,23 @@ "@deco/mcp": ["@jsr/deco__mcp@0.5.5", "https://npm.jsr.io/~/11/@jsr/deco__mcp/0.5.5.tgz", { "dependencies": { "@jsr/deco__deco": "^1.112.1", "@jsr/hono__hono": "^4.5.4", "@modelcontextprotocol/sdk": "^1.11.4", "fetch-to-node": "^2.1.0", "zod": "^3.24.2" } }, "sha512-46TaWGu7lbsPleHjCVrG6afhQjv3muBTNRFBkIhLrSzlQ+9d21UeukpYs19z0AGpOlmjSSK9qIRFTf8SlH2B6Q=="], - "@decocms/bindings": ["@decocms/bindings@1.0.7", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.1", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-NPYv4+VpI6XQbfMewy307Q1jp9QZc8a6lsC2g9Z/DCewKqFOCqAKsRrhBSGaujKEzHqxNLSqXhFx8/Vn3ODVJA=="], + "@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], + + "@decocms/blog": ["@decocms/blog@workspace:blog"], + + "@decocms/bookmarks": ["@decocms/bookmarks@workspace:bookmarks"], + + "@decocms/brand": ["@decocms/brand@workspace:brand"], + + "@decocms/mcp-local-fs": ["@decocms/mcp-local-fs@workspace:local-fs"], "@decocms/mcps-shared": ["@decocms/mcps-shared@workspace:shared"], "@decocms/openrouter": ["@decocms/openrouter@workspace:openrouter"], - "@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], + "@decocms/runtime": ["@decocms/runtime@1.2.0", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-HkhKBrCGYPlKyYW+iZxl7hmSwvzeJbBkFYE1+/0y5Ok1rlf/pxHHBHfiFlVgs1pr/seyFjEsqi2LnK8bMnDOEg=="], + + "@decocms/slides": ["@decocms/slides@workspace:slides"], "@decocms/vite-plugin": ["@decocms/vite-plugin@1.0.0-alpha.1", "", { "dependencies": { "@cloudflare/vite-plugin": "^1.13.4", "vite": "7.2.0" } }, "sha512-DI9zNH49pVk8aQ+7rNYwqTZhjQ4RZDA+kA1t3ifwc4RLJsOtYv8LOXERRZnou7CcKVTdXPB06M8gbMWPpSaq8w=="], @@ -1272,10 +1299,6 @@ "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], - "@langchain/core": ["@langchain/core@0.3.80", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": "^0.3.67", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", "uuid": "^10.0.0", "zod": "^3.25.32", "zod-to-json-schema": "^3.22.3" } }, "sha512-vcJDV2vk1AlCwSh3aBm/urQ1ZrlXFFBocv11bz/NBUfLWD5/UDNMzwPdaAd2dKvNmTWa9FM2lirLU3+JCf4cRA=="], - - "@langchain/textsplitters": ["@langchain/textsplitters@0.1.0", "", { "dependencies": { "js-tiktoken": "^1.0.12" }, "peerDependencies": { "@langchain/core": ">=0.2.21 <0.4.0" } }, "sha512-djI4uw9rlkAb5iMhtLED+xJebDdAG935AdP4eRTB02R7OB/act55Bj9wsskhZsvuyQRpO4O1wQOp85s6T6GWmw=="], - "@mastra/cloudflare-d1": ["@mastra/cloudflare-d1@0.13.10", "", { "dependencies": { "cloudflare": "^4.5.0" }, "peerDependencies": { "@mastra/core": ">=0.18.1-0 <0.25.0-0" } }, "sha512-Y+cFg9tNUACm/C3O8xavMrY9ydHgXXWxXJriaBQLVWNvuHD87YfjZjdFHK9AyRlVSsq0qbvM//v3ZH/fDNXaMQ=="], "@mastra/core": ["@mastra/core@0.24.9", "", { "dependencies": { "@a2a-js/sdk": "~0.2.4", "@ai-sdk/anthropic-v5": "npm:@ai-sdk/anthropic@2.0.33", "@ai-sdk/google-v5": "npm:@ai-sdk/google@2.0.40", "@ai-sdk/mistral-v5": "npm:@ai-sdk/mistral@2.0.23", "@ai-sdk/openai-compatible-v5": "npm:@ai-sdk/openai-compatible@1.0.22", "@ai-sdk/openai-v5": "npm:@ai-sdk/openai@2.0.53", "@ai-sdk/provider": "^1.1.3", "@ai-sdk/provider-utils": "^2.2.8", "@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.12", "@ai-sdk/provider-v5": "npm:@ai-sdk/provider@2.0.0", "@ai-sdk/ui-utils": "^1.2.11", "@ai-sdk/xai-v5": "npm:@ai-sdk/xai@2.0.26", "@isaacs/ttlcache": "^1.4.1", "@mastra/schema-compat": "0.11.9", "@openrouter/ai-sdk-provider-v5": "npm:@openrouter/ai-sdk-provider@1.2.3", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.62.1", "@opentelemetry/core": "^2.0.1", "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0", "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", "@opentelemetry/otlp-exporter-base": "^0.203.0", "@opentelemetry/otlp-transformer": "^0.203.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", "@opentelemetry/sdk-node": "^0.203.0", "@opentelemetry/sdk-trace-base": "^2.0.1", "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.36.0", "@sindresorhus/slugify": "^2.2.1", "ai": "^4.3.19", "ai-v5": "npm:ai@5.0.97", "date-fns": "^3.6.0", "dotenv": "^16.6.1", "hono": "^4.9.7", "hono-openapi": "^0.4.8", "js-tiktoken": "^1.0.20", "json-schema": "^0.4.0", "lru-cache": "^11.2.2", "p-map": "^7.0.3", "p-retry": "^7.1.0", "pino": "^9.7.0", "pino-pretty": "^13.0.0", "radash": "^12.1.1", "sift": "^17.1.3", "xstate": "^5.20.1", "zod-to-json-schema": "^3.24.6" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-EjAPnX6pq7Y+7YN/kO92HIHqfk9Z/jtggE4Ww6wiL2Gvr01eFoNZSmsrIT4vTQAdD4oM41R2x1ndgtKFAJRH0w=="], @@ -1896,7 +1919,7 @@ "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - "@types/bun": ["@types/bun@1.3.7", "", { "dependencies": { "bun-types": "1.3.7" } }, "sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA=="], + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], "@types/bunyan": ["@types/bunyan@1.8.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ=="], @@ -1952,7 +1975,7 @@ "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], - "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], @@ -1960,8 +1983,6 @@ "@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="], - "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@upstash/redis": ["@upstash/redis@1.36.1", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-N6SjDcgXdOcTAF+7uNoY69o7hCspe9BcA7YjQdxVu5d25avljTwyLaHBW3krWjrP0FfocgMk94qyVtQbeDp39A=="], @@ -2066,8 +2087,6 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="], "caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="], @@ -2114,8 +2133,6 @@ "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], - "console-table-printer": ["console-table-printer@2.15.0", "", { "dependencies": { "simple-wcswidth": "^1.1.2" } }, "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw=="], - "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], "content-scraper": ["content-scraper@workspace:content-scraper"], @@ -2148,8 +2165,6 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], - "deco-cli": ["deco-cli@0.28.6", "", { "dependencies": { "@deco-cx/warp-node": "0.3.16", "@modelcontextprotocol/sdk": "1.25.1", "@supabase/ssr": "0.6.1", "@supabase/supabase-js": "2.50.0", "chalk": "^5.3.0", "commander": "^12.0.0", "glob": "^10.3.10", "ignore": "^7.0.5", "inquirer": "^9.2.15", "inquirer-search-checkbox": "^1.0.0", "inquirer-search-list": "^1.2.6", "jose": "^6.0.11", "json-schema-to-typescript": "^15.0.4", "object-hash": "^3.0.0", "prettier": "^3.6.2", "semver": "^7.6.0", "smol-toml": "^1.3.4", "zod": "^3.25.76" }, "bin": { "deco": "dist/cli.js", "deconfig": "dist/deconfig.js" } }, "sha512-IwdfHoZfrLVGTVULBJ2NRjEkD9dZafJSf3qYsZeer7CR5owQ1XLnDAKIwd/c6iwLZB6+2zrMjL4RNWhF2SzZbw=="], "deco-llm": ["deco-llm@workspace:deco-llm"], @@ -2326,8 +2341,6 @@ "goober": ["goober@2.1.18", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw=="], - "google-apps-script": ["google-apps-script@workspace:google-apps-script"], - "google-big-query": ["google-big-query@workspace:google-big-query"], "google-calendar": ["google-calendar@workspace:google-calendar"], @@ -2476,8 +2489,6 @@ "kysely-bun-worker": ["kysely-bun-worker@0.6.3", "", { "peerDependencies": { "kysely": ">=0.26" } }, "sha512-gn1LqOsxsm7Bwv2c5Y+Mz07z8zKetqQMeSq6OLE5VUcU5NFEhv1TuPxV9AlwCuz8YX5vjkf5un4IZp2ATiJPhw=="], - "langsmith": ["langsmith@0.3.87", "", { "dependencies": { "@types/uuid": "^10.0.0", "chalk": "^4.1.2", "console-table-printer": "^2.12.1", "p-queue": "^6.6.2", "semver": "^7.6.3", "uuid": "^10.0.0" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-proto", "@opentelemetry/sdk-trace-base", "openai"] }, "sha512-XXR1+9INH8YX96FKWc5tie0QixWz6tOqAsAKfcJyPkE0xPep+NDz0IQLR32q4bn10QK3LqD2HN6T3n6z1YLW7Q=="], - "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], @@ -2576,8 +2587,6 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], - "mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], @@ -2860,8 +2869,6 @@ "simple-git": ["simple-git@3.30.0", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "debug": "^4.4.0" } }, "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg=="], - "simple-wcswidth": ["simple-wcswidth@1.1.2", "", {}, "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw=="], - "slack-mcp": ["slack-mcp@workspace:slack-mcp"], "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="], @@ -3006,8 +3013,6 @@ "vite": ["vite@7.2.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-C/Naxf8H0pBx1PA4BdpT+c/5wdqI9ILMdwjSMILw7tVIh3JsxzZqdeTLmmdaoh5MYUEOyBnM9K3o0DzoZ/fe+w=="], - "vtex-docs": ["vtex-docs@workspace:vtex-docs"], - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], "weak-lru-cache": ["weak-lru-cache@1.0.0", "", {}, "sha512-135bPugHHIJLNx20guHgk4etZAbd7nou34NQfdKkJPgMuC3Oqn4cT6f7ORVvnud9oEyXJVJXPcTFsUvttGm5xg=="], @@ -3016,8 +3021,6 @@ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "whatsapp-management": ["whatsapp-management@workspace:whatsapp-management"], - "whatsappagent": ["whatsappagent@workspace:whatsapp"], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], @@ -3082,17 +3085,9 @@ "@ai-sdk/mistral-v5/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.16", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-lsWQY9aDXHitw7C1QRYIbVGmgwyT98TF3MfM8alNIXKpdJdi+W782Rzd9f1RyOfgRmZ08gJ2EYNDhWNK7RqpEA=="], - "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], - - "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], - - "@ai-sdk/openai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], + "@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], - "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], - - "@ai-sdk/openai-compatible/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.12", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZtbdvYxdMoria+2SlNarEk6Hlgyf+zzcznlD55EAl+7VZvJaSg2sqPvwArY7L6TfDEDJsnCq0fdhBSkYo0Xqdg=="], "@ai-sdk/openai-compatible-v5/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], @@ -3112,8 +3107,6 @@ "@ai-sdk/ui-utils/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@ai-sdk/xai-v5/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.12" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Q+lwBIeMprc/iM+vg1yGjvzRrp74l316wDpqWdbmd4VXXlllblzGsUgBLTeKvcEapFTgqk0FRETvSb58Y6dsfA=="], - "@ai-sdk/xai-v5/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], "@ai-sdk/xai-v5/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.12", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZtbdvYxdMoria+2SlNarEk6Hlgyf+zzcznlD55EAl+7VZvJaSg2sqPvwArY7L6TfDEDJsnCq0fdhBSkYo0Xqdg=="], @@ -3144,9 +3137,19 @@ "@deco/mcp/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@decocms/openrouter/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], + "@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], + + "@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], + + "@decocms/mcp-local-fs/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], - "@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], + "@decocms/mcp-local-fs/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@decocms/mcps-shared/@decocms/bindings": ["@decocms/bindings@1.0.7", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.1", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-NPYv4+VpI6XQbfMewy307Q1jp9QZc8a6lsC2g9Z/DCewKqFOCqAKsRrhBSGaujKEzHqxNLSqXhFx8/Vn3ODVJA=="], + + "@decocms/mcps-shared/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], + + "@decocms/openrouter/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], @@ -3154,8 +3157,6 @@ "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], - "@discordjs/ws/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "@elevenlabs/elevenlabs-js/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -3232,14 +3233,6 @@ "@jsr/zaubrik__djwt/@jsr/std__encoding": ["@jsr/std__encoding@0.224.0", "https://npm.jsr.io/~/11/@jsr/std__encoding/0.224.0.tgz", {}, "sha512-V13A1JV6kvtlCyxeznQM8qYSPep5fiMfe59dnYD9gx//3TCHnGoqP2588qPDBGeD8IDrSUId3Czg/nGbcPt9Dw=="], - "@langchain/core/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "@langchain/core/p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], - - "@langchain/core/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - - "@langchain/core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@mastra/core/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], "@mastra/core/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], @@ -3392,7 +3385,7 @@ "@ts-morph/common/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], - "@types/bun/bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], + "@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], "ai-v5/@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.12", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W+cB1sOWvPcz9qiIsNtD+HxUrBUva2vWv2K1EFukuImX+HA0uZx3EyyOjhYQ9gtf/teqEG80M6OvJ7xx/VLV2A=="], @@ -3406,6 +3399,8 @@ "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "blog-post-generator/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "cloudflare/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], @@ -3414,6 +3409,8 @@ "concurrently/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "content-scraper/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], + "content-scraper/deco-cli": ["deco-cli@0.26.0", "", { "dependencies": { "@deco-cx/warp-node": "0.3.16", "@modelcontextprotocol/sdk": "^1.19.1", "@supabase/ssr": "0.6.1", "@supabase/supabase-js": "2.50.0", "chalk": "^5.3.0", "commander": "^12.0.0", "glob": "^10.3.10", "ignore": "^7.0.5", "inquirer": "^9.2.15", "inquirer-search-checkbox": "^1.0.0", "inquirer-search-list": "^1.2.6", "jose": "^6.0.11", "json-schema-to-typescript": "^15.0.4", "object-hash": "^3.0.0", "prettier": "^3.6.2", "semver": "^7.6.0", "smol-toml": "^1.3.4", "ws": "^8.16.0", "zod": "^3.25.76" }, "bin": { "deco": "dist/cli.js", "deconfig": "dist/deconfig.js" } }, "sha512-fkYKYO81cK3NE4hb3zcPdMksKJiYM2mon0lKGBuvEOruVUfbhK0I7V777NZDrmaxVQXxDx0fa9i6fARjxT7muQ=="], "data-for-seo/@decocms/runtime": ["@decocms/runtime@0.24.0", "", { "dependencies": { "@cloudflare/workers-types": "^4.20250617.0", "@deco/mcp": "npm:@jsr/deco__mcp@0.5.5", "@mastra/cloudflare-d1": "^0.13.4", "@mastra/core": "^0.20.2", "@modelcontextprotocol/sdk": "^1.19.1", "bidc": "0.0.3", "drizzle-orm": "^0.44.5", "jose": "^6.0.11", "mime-db": "1.52.0", "zod": "^3.25.76", "zod-from-json-schema": "^0.0.5", "zod-to-json-schema": "^3.24.4" } }, "sha512-ZWa9z6I0dl4LtVnv3NUDvxuVYU0Aka1gpUEkpJP0tW2ETCGQkmDx50MdFqEksXiL1RHoNZuv45Fz8u9FkdTKJg=="], @@ -3432,11 +3429,11 @@ "deco-cli/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "deco-llm/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], + "deco-llm/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - "discord-read/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], + "discord-read/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], "external-editor/chardet": ["chardet@0.4.2", "", {}, "sha512-j/Toj7f1z98Hh2cYo2BVr85EpIRWqUi7rtRSGxh/cqUjqrnJe9l9UE7IUGd2vQ2p+kSHLkSzObQPZPLUC6TQwg=="], @@ -3456,9 +3453,11 @@ "gemini-pro-vision/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "github/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], + "github/@decocms/bindings": ["@decocms/bindings@1.0.7", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.1", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-NPYv4+VpI6XQbfMewy307Q1jp9QZc8a6lsC2g9Z/DCewKqFOCqAKsRrhBSGaujKEzHqxNLSqXhFx8/Vn3ODVJA=="], + + "github/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], - "google-apps-script/@decocms/runtime": ["@decocms/runtime@1.2.5", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.1.1", "@modelcontextprotocol/sdk": "1.25.2", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-0s02lfj/O7nTAc7FTmFsA+lZpUDnapjQHnRYrQXItLKrbJvjSnfoq5V8HA1Npv5HelBvsVk7QQHaW8pSN/l37w=="], + "github/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], "google-big-query/@decocms/runtime": ["@decocms/runtime@1.2.5", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.1.1", "@modelcontextprotocol/sdk": "1.25.2", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-0s02lfj/O7nTAc7FTmFsA+lZpUDnapjQHnRYrQXItLKrbJvjSnfoq5V8HA1Npv5HelBvsVk7QQHaW8pSN/l37w=="], @@ -3490,7 +3489,7 @@ "http-response-object/@types/node": ["@types/node@10.17.60", "", {}, "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="], - "hyperdx/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], + "hyperdx/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], "hyperdx/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -3506,13 +3505,9 @@ "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "langsmith/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "langsmith/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "mcp-studio/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], + "mcp-studio/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], "mcp-template-minimal/@decocms/runtime": ["@decocms/runtime@0.25.1", "", { "dependencies": { "@cloudflare/workers-types": "^4.20250617.0", "@deco/mcp": "npm:@jsr/deco__mcp@0.5.5", "@mastra/cloudflare-d1": "^0.13.4", "@mastra/core": "^0.20.2", "@modelcontextprotocol/sdk": "^1.19.1", "bidc": "0.0.3", "drizzle-orm": "^0.44.5", "jose": "^6.0.11", "mime-db": "1.52.0", "zod": "^3.25.76", "zod-from-json-schema": "^0.0.5", "zod-to-json-schema": "^3.24.4" } }, "sha512-G1J09NpHkuOcBQMPDi7zJDwtNweH33/39sOsR/mpA+sRWn2W3CX51FXeB5dp06oAmCe9BoBpYnyvb896hSQ+Jg=="], @@ -3526,6 +3521,8 @@ "mcp-template-with-view/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "meta-ads/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -3538,6 +3535,8 @@ "nanobanana/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "object-storage/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], + "ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], @@ -3568,7 +3567,7 @@ "reddit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "registry/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], + "registry/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], "registry/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], @@ -3584,8 +3583,6 @@ "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - "slack-mcp/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - "slack-mcp/@decocms/runtime": ["@decocms/runtime@1.2.6", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.3", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-Eyp9pWkhlFLU3iQl/dQEULcUnirwcggKo3TEH0l5fLpkYoIDY48lW360T9iNm2Cd2ic1jRO13+rpo5eUTYU39w=="], "sora/@decocms/runtime": ["@decocms/runtime@0.25.1", "", { "dependencies": { "@cloudflare/workers-types": "^4.20250617.0", "@deco/mcp": "npm:@jsr/deco__mcp@0.5.5", "@mastra/cloudflare-d1": "^0.13.4", "@mastra/core": "^0.20.2", "@modelcontextprotocol/sdk": "^1.19.1", "bidc": "0.0.3", "drizzle-orm": "^0.44.5", "jose": "^6.0.11", "mime-db": "1.52.0", "zod": "^3.25.76", "zod-from-json-schema": "^0.0.5", "zod-to-json-schema": "^3.24.4" } }, "sha512-G1J09NpHkuOcBQMPDi7zJDwtNweH33/39sOsR/mpA+sRWn2W3CX51FXeB5dp06oAmCe9BoBpYnyvb896hSQ+Jg=="], @@ -3598,21 +3595,15 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "tiktok-ads/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], + "veo/@decocms/runtime": ["@decocms/runtime@0.25.1", "", { "dependencies": { "@cloudflare/workers-types": "^4.20250617.0", "@deco/mcp": "npm:@jsr/deco__mcp@0.5.5", "@mastra/cloudflare-d1": "^0.13.4", "@mastra/core": "^0.20.2", "@modelcontextprotocol/sdk": "^1.19.1", "bidc": "0.0.3", "drizzle-orm": "^0.44.5", "jose": "^6.0.11", "mime-db": "1.52.0", "zod": "^3.25.76", "zod-from-json-schema": "^0.0.5", "zod-to-json-schema": "^3.24.4" } }, "sha512-G1J09NpHkuOcBQMPDi7zJDwtNweH33/39sOsR/mpA+sRWn2W3CX51FXeB5dp06oAmCe9BoBpYnyvb896hSQ+Jg=="], "veo/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.20.2", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg=="], "veo/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "vtex-docs/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.20.2", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg=="], - - "vtex-docs/ai": ["ai@4.3.19", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q=="], - - "whatsapp-management/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - - "whatsapp-management/deco-cli": ["deco-cli@0.28.5", "", { "dependencies": { "@deco-cx/warp-node": "0.3.16", "@modelcontextprotocol/sdk": "1.20.2", "@supabase/ssr": "0.6.1", "@supabase/supabase-js": "2.50.0", "chalk": "^5.3.0", "commander": "^12.0.0", "glob": "^10.3.10", "ignore": "^7.0.5", "inquirer": "^9.2.15", "inquirer-search-checkbox": "^1.0.0", "inquirer-search-list": "^1.2.6", "jose": "^6.0.11", "json-schema-to-typescript": "^15.0.4", "object-hash": "^3.0.0", "prettier": "^3.6.2", "semver": "^7.6.0", "smol-toml": "^1.3.4", "zod": "^3.25.76" }, "bin": { "deco": "dist/cli.js", "deconfig": "dist/deconfig.js" } }, "sha512-DDzOPKrvMhoS6lu9u5nM8bP7LABClh8RKsVa6wHY+I6PUOtjKuk/mAgxJOt1uO9q2Ku9sgPle9FOyE/crM0Iqg=="], - - "whatsappagent/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], + "whatsappagent/@decocms/runtime": ["@decocms/runtime@1.1.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@decocms/bindings": "^1.0.7", "@modelcontextprotocol/sdk": "1.25.1", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^4.0.0" } }, "sha512-pj6OccmpWulMjyOt9FHTNX41xHk800uzhmoYK/xq9h1f+DfDBMqyOZnt70Zmwrab7dMDO2w3VQD+NlgFKtrw1w=="], "whatsappagent/deco-cli": ["deco-cli@0.28.5", "", { "dependencies": { "@deco-cx/warp-node": "0.3.16", "@modelcontextprotocol/sdk": "1.20.2", "@supabase/ssr": "0.6.1", "@supabase/supabase-js": "2.50.0", "chalk": "^5.3.0", "commander": "^12.0.0", "glob": "^10.3.10", "ignore": "^7.0.5", "inquirer": "^9.2.15", "inquirer-search-checkbox": "^1.0.0", "inquirer-search-list": "^1.2.6", "jose": "^6.0.11", "json-schema-to-typescript": "^15.0.4", "object-hash": "^3.0.0", "prettier": "^3.6.2", "semver": "^7.6.0", "smol-toml": "^1.3.4", "zod": "^3.25.76" }, "bin": { "deco": "dist/cli.js", "deconfig": "dist/deconfig.js" } }, "sha512-DDzOPKrvMhoS6lu9u5nM8bP7LABClh8RKsVa6wHY+I6PUOtjKuk/mAgxJOt1uO9q2Ku9sgPle9FOyE/crM0Iqg=="], @@ -3670,13 +3661,13 @@ "@deco/mcp/@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@decocms/openrouter/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], + "@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - "@decocms/openrouter/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], + "@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - "@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], + "@decocms/mcp-local-fs/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], + "@decocms/mcps-shared/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -3768,16 +3759,12 @@ "ai-v5/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], - "apify/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - "apify/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], "cloudflare/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "concurrently/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "content-scraper/deco-cli/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - "content-scraper/deco-cli/@supabase/supabase-js": ["@supabase/supabase-js@2.50.0", "", { "dependencies": { "@supabase/auth-js": "2.70.0", "@supabase/functions-js": "2.4.4", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.19.4", "@supabase/realtime-js": "2.11.10", "@supabase/storage-js": "2.7.1" } }, "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg=="], "content-scraper/deco-cli/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], @@ -3810,14 +3797,6 @@ "deco-cli/@supabase/supabase-js/@supabase/storage-js": ["@supabase/storage-js@2.7.1", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA=="], - "deco-llm/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "deco-llm/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "discord-read/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "discord-read/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - "gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "gemini-pro-vision/@decocms/runtime/@mastra/core": ["@mastra/core@0.20.2", "", { "dependencies": { "@a2a-js/sdk": "~0.2.4", "@ai-sdk/anthropic-v5": "npm:@ai-sdk/anthropic@2.0.23", "@ai-sdk/google-v5": "npm:@ai-sdk/google@2.0.17", "@ai-sdk/openai-compatible-v5": "npm:@ai-sdk/openai-compatible@1.0.19", "@ai-sdk/openai-v5": "npm:@ai-sdk/openai@2.0.42", "@ai-sdk/provider": "^1.1.3", "@ai-sdk/provider-utils": "^2.2.8", "@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.10", "@ai-sdk/provider-v5": "npm:@ai-sdk/provider@2.0.0", "@ai-sdk/ui-utils": "^1.2.11", "@ai-sdk/xai-v5": "npm:@ai-sdk/xai@2.0.23", "@isaacs/ttlcache": "^1.4.1", "@mastra/schema-compat": "0.11.4", "@openrouter/ai-sdk-provider-v5": "npm:@openrouter/ai-sdk-provider@1.2.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.62.1", "@opentelemetry/core": "^2.0.1", "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0", "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", "@opentelemetry/otlp-exporter-base": "^0.203.0", "@opentelemetry/otlp-transformer": "^0.203.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", "@opentelemetry/sdk-node": "^0.203.0", "@opentelemetry/sdk-trace-base": "^2.0.1", "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.36.0", "@sindresorhus/slugify": "^2.2.1", "ai": "^4.3.19", "ai-v5": "npm:ai@5.0.60", "date-fns": "^3.6.0", "dotenv": "^16.6.1", "hono": "^4.9.7", "hono-openapi": "^0.4.8", "js-tiktoken": "^1.0.20", "json-schema": "^0.4.0", "json-schema-to-zod": "^2.6.1", "p-map": "^7.0.3", "pino": "^9.7.0", "pino-pretty": "^13.0.0", "radash": "^12.1.1", "sift": "^17.1.3", "xstate": "^5.20.1", "zod-to-json-schema": "^3.24.6" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RbwuLwOVrcLbbjLFEBSlGTBA3mzGAy4bXp4JeXg2miJWDR/7WbXtxKIU+sTZGw5LpzlvvEFtj7JtHI1l+gKMVg=="], @@ -3828,50 +3807,28 @@ "gemini-pro-vision/@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "github/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "google-apps-script/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - - "google-apps-script/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], + "github/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - "google-big-query/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], + "github/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "google-big-query/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], - "google-calendar/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - "google-calendar/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], - "google-docs/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - "google-docs/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], - "google-drive/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - "google-drive/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], - "google-forms/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - "google-forms/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], - "google-gmail/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - "google-gmail/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], - "google-meet/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - "google-meet/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], - "google-sheets/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - "google-sheets/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], - "google-slides/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - "google-slides/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], - "google-tag-manager/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - "google-tag-manager/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], "grain/@decocms/bindings/zod-from-json-schema": ["zod-from-json-schema@0.0.5", "", { "dependencies": { "zod": "^3.24.2" } }, "sha512-zYEoo86M1qpA1Pq6329oSyHLS785z/mTwfr9V1Xf/ZLhuuBGaMlDGu/pDVGVUe4H4oa1EFgWZT53DP0U3oT9CQ=="], @@ -3884,11 +3841,7 @@ "grain/@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "hyperdx/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "hyperdx/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "hyperdx/@decocms/bindings/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "hyperdx/@decocms/runtime/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "inquirer-search-checkbox/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], @@ -3934,14 +3887,8 @@ "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "langsmith/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "mcp-studio/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "mcp-studio/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - "mcp-template-minimal/@decocms/runtime/@mastra/core": ["@mastra/core@0.20.2", "", { "dependencies": { "@a2a-js/sdk": "~0.2.4", "@ai-sdk/anthropic-v5": "npm:@ai-sdk/anthropic@2.0.23", "@ai-sdk/google-v5": "npm:@ai-sdk/google@2.0.17", "@ai-sdk/openai-compatible-v5": "npm:@ai-sdk/openai-compatible@1.0.19", "@ai-sdk/openai-v5": "npm:@ai-sdk/openai@2.0.42", "@ai-sdk/provider": "^1.1.3", "@ai-sdk/provider-utils": "^2.2.8", "@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.10", "@ai-sdk/provider-v5": "npm:@ai-sdk/provider@2.0.0", "@ai-sdk/ui-utils": "^1.2.11", "@ai-sdk/xai-v5": "npm:@ai-sdk/xai@2.0.23", "@isaacs/ttlcache": "^1.4.1", "@mastra/schema-compat": "0.11.4", "@openrouter/ai-sdk-provider-v5": "npm:@openrouter/ai-sdk-provider@1.2.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.62.1", "@opentelemetry/core": "^2.0.1", "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0", "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", "@opentelemetry/otlp-exporter-base": "^0.203.0", "@opentelemetry/otlp-transformer": "^0.203.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", "@opentelemetry/sdk-node": "^0.203.0", "@opentelemetry/sdk-trace-base": "^2.0.1", "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.36.0", "@sindresorhus/slugify": "^2.2.1", "ai": "^4.3.19", "ai-v5": "npm:ai@5.0.60", "date-fns": "^3.6.0", "dotenv": "^16.6.1", "hono": "^4.9.7", "hono-openapi": "^0.4.8", "js-tiktoken": "^1.0.20", "json-schema": "^0.4.0", "json-schema-to-zod": "^2.6.1", "p-map": "^7.0.3", "pino": "^9.7.0", "pino-pretty": "^13.0.0", "radash": "^12.1.1", "sift": "^17.1.3", "xstate": "^5.20.1", "zod-to-json-schema": "^3.24.6" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RbwuLwOVrcLbbjLFEBSlGTBA3mzGAy4bXp4JeXg2miJWDR/7WbXtxKIU+sTZGw5LpzlvvEFtj7JtHI1l+gKMVg=="], "mcp-template-minimal/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], @@ -3968,8 +3915,6 @@ "ora/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "perplexity/@decocms/runtime/@decocms/bindings": ["@decocms/bindings@1.2.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.25.3", "@tanstack/react-router": "1.139.7", "react": "^19.2.0", "zod": "^4.0.0", "zod-from-json-schema": "^0.5.2" } }, "sha512-+4/VOOVERB8UixGKmN0VkLazxeMAahbG0A9xOYTPL+MJIAM30htrLG2aHI2Dm5ASgccAD4bW5RuLqv2PDFZZPA=="], - "perplexity/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.2", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww=="], "pinecone/@decocms/runtime/@mastra/core": ["@mastra/core@0.20.2", "", { "dependencies": { "@a2a-js/sdk": "~0.2.4", "@ai-sdk/anthropic-v5": "npm:@ai-sdk/anthropic@2.0.23", "@ai-sdk/google-v5": "npm:@ai-sdk/google@2.0.17", "@ai-sdk/openai-compatible-v5": "npm:@ai-sdk/openai-compatible@1.0.19", "@ai-sdk/openai-v5": "npm:@ai-sdk/openai@2.0.42", "@ai-sdk/provider": "^1.1.3", "@ai-sdk/provider-utils": "^2.2.8", "@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.10", "@ai-sdk/provider-v5": "npm:@ai-sdk/provider@2.0.0", "@ai-sdk/ui-utils": "^1.2.11", "@ai-sdk/xai-v5": "npm:@ai-sdk/xai@2.0.23", "@isaacs/ttlcache": "^1.4.1", "@mastra/schema-compat": "0.11.4", "@openrouter/ai-sdk-provider-v5": "npm:@openrouter/ai-sdk-provider@1.2.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.62.1", "@opentelemetry/core": "^2.0.1", "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0", "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", "@opentelemetry/otlp-exporter-base": "^0.203.0", "@opentelemetry/otlp-transformer": "^0.203.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", "@opentelemetry/sdk-node": "^0.203.0", "@opentelemetry/sdk-trace-base": "^2.0.1", "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.36.0", "@sindresorhus/slugify": "^2.2.1", "ai": "^4.3.19", "ai-v5": "npm:ai@5.0.60", "date-fns": "^3.6.0", "dotenv": "^16.6.1", "hono": "^4.9.7", "hono-openapi": "^0.4.8", "js-tiktoken": "^1.0.20", "json-schema": "^0.4.0", "json-schema-to-zod": "^2.6.1", "p-map": "^7.0.3", "pino": "^9.7.0", "pino-pretty": "^13.0.0", "radash": "^12.1.1", "sift": "^17.1.3", "xstate": "^5.20.1", "zod-to-json-schema": "^3.24.6" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RbwuLwOVrcLbbjLFEBSlGTBA3mzGAy4bXp4JeXg2miJWDR/7WbXtxKIU+sTZGw5LpzlvvEFtj7JtHI1l+gKMVg=="], @@ -3994,16 +3939,10 @@ "reddit/@decocms/runtime/zod-from-json-schema": ["zod-from-json-schema@0.0.5", "", { "dependencies": { "zod": "^3.24.2" } }, "sha512-zYEoo86M1qpA1Pq6329oSyHLS785z/mTwfr9V1Xf/ZLhuuBGaMlDGu/pDVGVUe4H4oa1EFgWZT53DP0U3oT9CQ=="], - "reddit/deco-cli/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - "reddit/deco-cli/@supabase/supabase-js": ["@supabase/supabase-js@2.50.0", "", { "dependencies": { "@supabase/auth-js": "2.70.0", "@supabase/functions-js": "2.4.4", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.19.4", "@supabase/realtime-js": "2.11.10", "@supabase/storage-js": "2.7.1" } }, "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg=="], "reddit/deco-cli/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "registry/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "registry/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - "registry/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "replicate/@decocms/runtime/@mastra/core": ["@mastra/core@0.20.2", "", { "dependencies": { "@a2a-js/sdk": "~0.2.4", "@ai-sdk/anthropic-v5": "npm:@ai-sdk/anthropic@2.0.23", "@ai-sdk/google-v5": "npm:@ai-sdk/google@2.0.17", "@ai-sdk/openai-compatible-v5": "npm:@ai-sdk/openai-compatible@1.0.19", "@ai-sdk/openai-v5": "npm:@ai-sdk/openai@2.0.42", "@ai-sdk/provider": "^1.1.3", "@ai-sdk/provider-utils": "^2.2.8", "@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.10", "@ai-sdk/provider-v5": "npm:@ai-sdk/provider@2.0.0", "@ai-sdk/ui-utils": "^1.2.11", "@ai-sdk/xai-v5": "npm:@ai-sdk/xai@2.0.23", "@isaacs/ttlcache": "^1.4.1", "@mastra/schema-compat": "0.11.4", "@openrouter/ai-sdk-provider-v5": "npm:@openrouter/ai-sdk-provider@1.2.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.62.1", "@opentelemetry/core": "^2.0.1", "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0", "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", "@opentelemetry/otlp-exporter-base": "^0.203.0", "@opentelemetry/otlp-transformer": "^0.203.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", "@opentelemetry/sdk-node": "^0.203.0", "@opentelemetry/sdk-trace-base": "^2.0.1", "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.36.0", "@sindresorhus/slugify": "^2.2.1", "ai": "^4.3.19", "ai-v5": "npm:ai@5.0.60", "date-fns": "^3.6.0", "dotenv": "^16.6.1", "hono": "^4.9.7", "hono-openapi": "^0.4.8", "js-tiktoken": "^1.0.20", "json-schema": "^0.4.0", "json-schema-to-zod": "^2.6.1", "p-map": "^7.0.3", "pino": "^9.7.0", "pino-pretty": "^13.0.0", "radash": "^12.1.1", "sift": "^17.1.3", "xstate": "^5.20.1", "zod-to-json-schema": "^3.24.6" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RbwuLwOVrcLbbjLFEBSlGTBA3mzGAy4bXp4JeXg2miJWDR/7WbXtxKIU+sTZGw5LpzlvvEFtj7JtHI1l+gKMVg=="], @@ -4014,10 +3953,6 @@ "replicate/@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "slack-mcp/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "slack-mcp/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - "slack-mcp/@decocms/runtime/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], "sora/@decocms/runtime/@mastra/core": ["@mastra/core@0.20.2", "", { "dependencies": { "@a2a-js/sdk": "~0.2.4", "@ai-sdk/anthropic-v5": "npm:@ai-sdk/anthropic@2.0.23", "@ai-sdk/google-v5": "npm:@ai-sdk/google@2.0.17", "@ai-sdk/openai-compatible-v5": "npm:@ai-sdk/openai-compatible@1.0.19", "@ai-sdk/openai-v5": "npm:@ai-sdk/openai@2.0.42", "@ai-sdk/provider": "^1.1.3", "@ai-sdk/provider-utils": "^2.2.8", "@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.10", "@ai-sdk/provider-v5": "npm:@ai-sdk/provider@2.0.0", "@ai-sdk/ui-utils": "^1.2.11", "@ai-sdk/xai-v5": "npm:@ai-sdk/xai@2.0.23", "@isaacs/ttlcache": "^1.4.1", "@mastra/schema-compat": "0.11.4", "@openrouter/ai-sdk-provider-v5": "npm:@openrouter/ai-sdk-provider@1.2.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.62.1", "@opentelemetry/core": "^2.0.1", "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0", "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", "@opentelemetry/otlp-exporter-base": "^0.203.0", "@opentelemetry/otlp-transformer": "^0.203.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", "@opentelemetry/sdk-node": "^0.203.0", "@opentelemetry/sdk-trace-base": "^2.0.1", "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.36.0", "@sindresorhus/slugify": "^2.2.1", "ai": "^4.3.19", "ai-v5": "npm:ai@5.0.60", "date-fns": "^3.6.0", "dotenv": "^16.6.1", "hono": "^4.9.7", "hono-openapi": "^0.4.8", "js-tiktoken": "^1.0.20", "json-schema": "^0.4.0", "json-schema-to-zod": "^2.6.1", "p-map": "^7.0.3", "pino": "^9.7.0", "pino-pretty": "^13.0.0", "radash": "^12.1.1", "sift": "^17.1.3", "xstate": "^5.20.1", "zod-to-json-schema": "^3.24.6" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RbwuLwOVrcLbbjLFEBSlGTBA3mzGAy4bXp4JeXg2miJWDR/7WbXtxKIU+sTZGw5LpzlvvEFtj7JtHI1l+gKMVg=="], @@ -4036,30 +3971,6 @@ "veo/@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "vtex-docs/@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - - "vtex-docs/@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "vtex-docs/ai/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], - - "vtex-docs/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], - - "vtex-docs/ai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "whatsapp-management/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "whatsapp-management/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "whatsapp-management/deco-cli/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.20.2", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg=="], - - "whatsapp-management/deco-cli/@supabase/supabase-js": ["@supabase/supabase-js@2.50.0", "", { "dependencies": { "@supabase/auth-js": "2.70.0", "@supabase/functions-js": "2.4.4", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.19.4", "@supabase/realtime-js": "2.11.10", "@supabase/storage-js": "2.7.1" } }, "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg=="], - - "whatsapp-management/deco-cli/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "whatsappagent/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "whatsappagent/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - "whatsappagent/deco-cli/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.20.2", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg=="], "whatsappagent/deco-cli/@supabase/supabase-js": ["@supabase/supabase-js@2.50.0", "", { "dependencies": { "@supabase/auth-js": "2.70.0", "@supabase/functions-js": "2.4.4", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.19.4", "@supabase/realtime-js": "2.11.10", "@supabase/storage-js": "2.7.1" } }, "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg=="], @@ -4148,13 +4059,9 @@ "@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-POaGMcXnozzqBUyJM3HLUZ9GR6OKJWPGJEmhtTnxZXt8B6JcJ/6K3xRJ5H/j8oovVLz8Wg6vFxAHv8lvuASxMg=="], - "@decocms/openrouter/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "@decocms/openrouter/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], + "@decocms/mcps-shared/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - "@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], + "@decocms/mcps-shared/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], "@mastra/schema-compat/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], @@ -4162,12 +4069,6 @@ "@openrouter/ai-sdk-provider/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], - "apify/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "apify/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "content-scraper/deco-cli/@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "content-scraper/deco-cli/@supabase/supabase-js/@supabase/auth-js": ["@supabase/auth-js@2.70.0", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg=="], "content-scraper/deco-cli/@supabase/supabase-js/@supabase/functions-js": ["@supabase/functions-js@2.4.4", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA=="], @@ -4236,14 +4137,6 @@ "deco-cli/@supabase/supabase-js/@supabase/realtime-js/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "deco-llm/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "deco-llm/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - - "discord-read/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "discord-read/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - "gemini-pro-vision/@decocms/runtime/@mastra/core/@ai-sdk/anthropic-v5": ["@ai-sdk/anthropic@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.10" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZEBiiv1UhjGjBwUU63pFhLK5LCSlNDb1idY9K1oZHm5/Fda1cuTojf32tOp0opH0RPbPAN/F8fyyNjbU33n9Kw=="], "gemini-pro-vision/@decocms/runtime/@mastra/core/@ai-sdk/google-v5": ["@ai-sdk/google@2.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.10" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6LyuUrCZuiULg0rUV+kT4T2jG19oUntudorI4ttv1ARkSbwl8A39ue3rA487aDDy6fUScdbGFiV5Yv/o4gidVA=="], @@ -4272,58 +4165,14 @@ "gemini-pro-vision/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "google-apps-script/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "google-apps-script/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "google-big-query/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], + "github/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - "google-big-query/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "google-calendar/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "google-calendar/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "google-docs/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "google-docs/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "google-drive/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "google-drive/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "google-forms/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "google-forms/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "google-gmail/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "google-gmail/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "google-meet/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "google-meet/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "google-sheets/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "google-sheets/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "google-slides/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "google-slides/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - - "google-tag-manager/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "google-tag-manager/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], + "github/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], "grain/@decocms/runtime/@deco/mcp/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], "grain/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "hyperdx/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "hyperdx/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - "inquirer-search-checkbox/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "inquirer-search-checkbox/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], @@ -4344,10 +4193,6 @@ "inquirer-search-list/inquirer/strip-ansi/ansi-regex": ["ansi-regex@3.0.1", "", {}, "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw=="], - "mcp-studio/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "mcp-studio/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - "mcp-template-minimal/@decocms/runtime/@mastra/core/@ai-sdk/anthropic-v5": ["@ai-sdk/anthropic@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.10" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZEBiiv1UhjGjBwUU63pFhLK5LCSlNDb1idY9K1oZHm5/Fda1cuTojf32tOp0opH0RPbPAN/F8fyyNjbU33n9Kw=="], "mcp-template-minimal/@decocms/runtime/@mastra/core/@ai-sdk/google-v5": ["@ai-sdk/google@2.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.10" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6LyuUrCZuiULg0rUV+kT4T2jG19oUntudorI4ttv1ARkSbwl8A39ue3rA487aDDy6fUScdbGFiV5Yv/o4gidVA=="], @@ -4432,10 +4277,6 @@ "nanobanana/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "perplexity/@decocms/runtime/@decocms/bindings/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], - - "perplexity/@decocms/runtime/@decocms/bindings/@tanstack/react-router": ["@tanstack/react-router@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.139.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5vhwIAwoxWl7oeIZRNgk5wh9TCkaAinK9qbfdKuKzwGtMHqnv1bRrfKwam3/MaMwHCmvnNfnFj0RYfnBA/ilEg=="], - "pinecone/@decocms/runtime/@mastra/core/@ai-sdk/anthropic-v5": ["@ai-sdk/anthropic@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.10" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZEBiiv1UhjGjBwUU63pFhLK5LCSlNDb1idY9K1oZHm5/Fda1cuTojf32tOp0opH0RPbPAN/F8fyyNjbU33n9Kw=="], "pinecone/@decocms/runtime/@mastra/core/@ai-sdk/google-v5": ["@ai-sdk/google@2.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.10" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6LyuUrCZuiULg0rUV+kT4T2jG19oUntudorI4ttv1ARkSbwl8A39ue3rA487aDDy6fUScdbGFiV5Yv/o4gidVA=="], @@ -4518,8 +4359,6 @@ "reddit/@decocms/runtime/@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "reddit/deco-cli/@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "reddit/deco-cli/@supabase/supabase-js/@supabase/auth-js": ["@supabase/auth-js@2.70.0", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg=="], "reddit/deco-cli/@supabase/supabase-js/@supabase/functions-js": ["@supabase/functions-js@2.4.4", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA=="], @@ -4530,10 +4369,6 @@ "reddit/deco-cli/@supabase/supabase-js/@supabase/storage-js": ["@supabase/storage-js@2.7.1", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA=="], - "registry/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "registry/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - "replicate/@decocms/runtime/@mastra/core/@ai-sdk/anthropic-v5": ["@ai-sdk/anthropic@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.10" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZEBiiv1UhjGjBwUU63pFhLK5LCSlNDb1idY9K1oZHm5/Fda1cuTojf32tOp0opH0RPbPAN/F8fyyNjbU33n9Kw=="], "replicate/@decocms/runtime/@mastra/core/@ai-sdk/google-v5": ["@ai-sdk/google@2.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.10" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6LyuUrCZuiULg0rUV+kT4T2jG19oUntudorI4ttv1ARkSbwl8A39ue3rA487aDDy6fUScdbGFiV5Yv/o4gidVA=="], @@ -4562,10 +4397,6 @@ "replicate/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "slack-mcp/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "slack-mcp/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - "sora/@decocms/runtime/@mastra/core/@ai-sdk/anthropic-v5": ["@ai-sdk/anthropic@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.10" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZEBiiv1UhjGjBwUU63pFhLK5LCSlNDb1idY9K1oZHm5/Fda1cuTojf32tOp0opH0RPbPAN/F8fyyNjbU33n9Kw=="], "sora/@decocms/runtime/@mastra/core/@ai-sdk/google-v5": ["@ai-sdk/google@2.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.10" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6LyuUrCZuiULg0rUV+kT4T2jG19oUntudorI4ttv1ARkSbwl8A39ue3rA487aDDy6fUScdbGFiV5Yv/o4gidVA=="], @@ -4622,28 +4453,6 @@ "veo/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "vtex-docs/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "whatsapp-management/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "whatsapp-management/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - - "whatsapp-management/deco-cli/@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - - "whatsapp-management/deco-cli/@supabase/supabase-js/@supabase/auth-js": ["@supabase/auth-js@2.70.0", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg=="], - - "whatsapp-management/deco-cli/@supabase/supabase-js/@supabase/functions-js": ["@supabase/functions-js@2.4.4", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA=="], - - "whatsapp-management/deco-cli/@supabase/supabase-js/@supabase/postgrest-js": ["@supabase/postgrest-js@1.19.4", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw=="], - - "whatsapp-management/deco-cli/@supabase/supabase-js/@supabase/realtime-js": ["@supabase/realtime-js@2.11.10", "", { "dependencies": { "@supabase/node-fetch": "^2.6.13", "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "ws": "^8.18.2" } }, "sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA=="], - - "whatsapp-management/deco-cli/@supabase/supabase-js/@supabase/storage-js": ["@supabase/storage-js@2.7.1", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA=="], - - "whatsappagent/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "whatsappagent/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - "whatsappagent/deco-cli/@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "whatsappagent/deco-cli/@supabase/supabase-js/@supabase/auth-js": ["@supabase/auth-js@2.70.0", "", { "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg=="], @@ -4684,9 +4493,9 @@ "whisper/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "apify/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], + "@decocms/mcps-shared/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - "apify/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], + "@decocms/mcps-shared/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], "data-for-seo/@decocms/runtime/@mastra/core/@ai-sdk/anthropic-v5/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], @@ -4790,49 +4599,9 @@ "gemini-pro-vision/@decocms/runtime/@mastra/core/ai-v5/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-T1gZ76gEIwffep6MWI0QNy9jgoybUHE7TRaHB5k54K8mF91ciGFlbtCGxDYhMH3nCRergKwYFIDeFF0hJSIQHQ=="], - "google-apps-script/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "google-apps-script/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - - "google-big-query/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "google-big-query/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - - "google-calendar/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "google-calendar/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - - "google-docs/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "google-docs/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - - "google-drive/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], + "github/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - "google-drive/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - - "google-forms/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "google-forms/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - - "google-gmail/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "google-gmail/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - - "google-meet/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "google-meet/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - - "google-sheets/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "google-sheets/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - - "google-slides/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "google-slides/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - - "google-tag-manager/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "google-tag-manager/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], + "github/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], "grain/@decocms/runtime/@deco/mcp/@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], @@ -4952,10 +4721,6 @@ "nanobanana/@decocms/runtime/@mastra/core/ai-v5/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-T1gZ76gEIwffep6MWI0QNy9jgoybUHE7TRaHB5k54K8mF91ciGFlbtCGxDYhMH3nCRergKwYFIDeFF0hJSIQHQ=="], - "perplexity/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], - - "perplexity/@decocms/runtime/@decocms/bindings/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], - "pinecone/@decocms/runtime/@mastra/core/@ai-sdk/anthropic-v5/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], "pinecone/@decocms/runtime/@mastra/core/@ai-sdk/anthropic-v5/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-T1gZ76gEIwffep6MWI0QNy9jgoybUHE7TRaHB5k54K8mF91ciGFlbtCGxDYhMH3nCRergKwYFIDeFF0hJSIQHQ=="], @@ -5160,10 +4925,6 @@ "veo/@decocms/runtime/@mastra/core/ai-v5/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-T1gZ76gEIwffep6MWI0QNy9jgoybUHE7TRaHB5k54K8mF91ciGFlbtCGxDYhMH3nCRergKwYFIDeFF0hJSIQHQ=="], - "whatsapp-management/deco-cli/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "whatsapp-management/deco-cli/@supabase/supabase-js/@supabase/realtime-js/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "whatsappagent/deco-cli/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "whatsappagent/deco-cli/@supabase/supabase-js/@supabase/realtime-js/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], diff --git a/local-fs/README.md b/local-fs/README.md new file mode 100644 index 00000000..e1ae7b0d --- /dev/null +++ b/local-fs/README.md @@ -0,0 +1,196 @@ +# @decocms/mcp-local-fs + +Mount any local filesystem path as an MCP server. **Drop-in replacement** for the official MCP filesystem server, with additional MCP Mesh collection bindings. + +## Features + +- 📁 Mount any filesystem path dynamically +- 🔌 **Stdio transport** (default) - works with Claude Desktop, Cursor, and other MCP clients +- 🌐 **HTTP transport** - for MCP Mesh integration +- 🛠️ **Full MCP filesystem compatibility** - same tools as the official server +- 📋 **Collection bindings** for Files and Folders (Mesh-compatible) +- 🔄 **Backward compatible** - supports both official and Mesh tool names +- ⚡ Zero config needed + +## Quick Start + +### Using npx (stdio mode - recommended for Claude Desktop) + +```bash +# Mount current directory +npx @decocms/mcp-local-fs + +# Mount specific path +npx @decocms/mcp-local-fs /path/to/folder + +# Or with --path flag +npx @decocms/mcp-local-fs --path /path/to/folder +``` + +### Claude Desktop Configuration + +Add to your `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "local-fs": { + "command": "npx", + "args": ["@decocms/mcp-local-fs", "/path/to/folder"] + } + } +} +``` + +### Cursor Configuration + +Add to your Cursor MCP settings: + +```json +{ + "mcpServers": { + "local-fs": { + "command": "npx", + "args": ["@decocms/mcp-local-fs", "/path/to/folder"] + } + } +} +``` + +### Serve Mode with deco link (easiest for remote Mesh) + +**One command to expose your local files to Deco Mesh:** + +```bash +# Serve current directory with public URL +cd /path/to/your/project +bun run serve # if installed locally +bunx @decocms/mcp-local-fs serve # via bunx + +# Serve specific folder +bunx @decocms/mcp-local-fs serve /path/to/folder + +# With custom port +bunx @decocms/mcp-local-fs serve --port 8080 +``` + +This will: +1. Start the HTTP server locally +2. Create a public tunnel via `deco link` +3. Display a ready-to-add MCP URL +4. Copy the URL to your clipboard + +Just paste the URL in Deco Mesh > Connections > Add Custom MCP! + +### HTTP Mode (for local Mesh) + +```bash +# Start HTTP server on port 3456 +npx @decocms/mcp-local-fs --http + +# With custom port +npx @decocms/mcp-local-fs --http --port 8080 + +# Mount specific path +npx @decocms/mcp-local-fs --http --path /your/folder +``` + +Then connect using: +- `http://localhost:3456/mcp?path=/your/folder` +- `http://localhost:3456/mcp/your/folder` + +## Adding to MCP Mesh + +Add a new connection with: +- **Transport**: HTTP +- **URL**: `http://localhost:3456/mcp?path=/your/folder` + +Or use the path in URL format: +- **URL**: `http://localhost:3456/mcp/home/user/documents` + +## Available Tools + +### Official MCP Filesystem Tools + +These tools follow the exact same schema as the [official MCP filesystem server](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem): + +| Tool | Description | +|------|-------------| +| `read_file` | Read a file (deprecated, use `read_text_file`) | +| `read_text_file` | Read a text file with optional head/tail params | +| `read_media_file` | Read binary/media files as base64 | +| `read_multiple_files` | Read multiple files at once | +| `write_file` | Write content to a file | +| `edit_file` | Search/replace edits with diff preview | +| `create_directory` | Create a directory (with nested support) | +| `list_directory` | List files and directories | +| `list_directory_with_sizes` | List with file sizes | +| `directory_tree` | Recursive tree view as JSON | +| `move_file` | Move or rename files/directories | +| `search_files` | Search files by glob pattern | +| `get_file_info` | Get detailed file/directory metadata | +| `list_allowed_directories` | Show allowed directories | + +### Additional Tools + +| Tool | Description | +|------|-------------| +| `delete_file` | Delete a file or directory (with recursive option) | +| `copy_file` | Copy a file to a new location | + +### MCP Mesh Collection Bindings + +These tools provide standard collection bindings for MCP Mesh compatibility: + +| Tool | Description | +|------|-------------| +| `COLLECTION_FILES_LIST` | List files with pagination | +| `COLLECTION_FILES_GET` | Get file metadata and content by path | +| `COLLECTION_FOLDERS_LIST` | List folders with pagination | +| `COLLECTION_FOLDERS_GET` | Get folder metadata by path | + +### MCP Mesh Compatibility Aliases + +For backward compatibility with existing Mesh connections, these aliases are also available: + +| Mesh Tool | Maps To | +|-----------|---------| +| `FILE_READ` | `read_text_file` | +| `FILE_WRITE` | `write_file` | +| `FILE_DELETE` | `delete_file` | +| `FILE_MOVE` | `move_file` | +| `FILE_COPY` | `copy_file` | +| `FILE_MKDIR` | `create_directory` | + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `MCP_LOCAL_FS_PATH` | Default path to mount | +| `PORT` | HTTP server port (default: 3456) | + +## Development + +```bash +# Install dependencies +npm install + +# Run in stdio mode (development) +npm run dev:stdio + +# Run in http mode (development) +npm run dev + +# Run tests +npm test + +# Type check +npm run check + +# Build for distribution +npm run build +``` + +## License + +MIT diff --git a/local-fs/bun.lock b/local-fs/bun.lock new file mode 100644 index 00000000..6e9d4d35 --- /dev/null +++ b/local-fs/bun.lock @@ -0,0 +1,206 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "@decocms/mcp-local-fs", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.20.2", + "kill-my-port": "^1.1.2", + "zod": "^3.24.0", + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + }, + }, + }, + "packages": { + "@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, ""], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="], + + "@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, ""], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hono": ["hono@4.11.3", "", {}, ""], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "kill-my-port": ["kill-my-port@1.1.2", "", { "bin": { "kill-my-port": "index.js" } }, "sha512-8T/8GdIGL1Ia1BbKykztZZigVQ7gRckGYQ2bnCOPZ+V+QrpCEAxz4rtVSRZRUZwr+50fBnitIMM8qEtUS8ZWfQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""], + + "undici-types": ["undici-types@6.21.0", "", {}, ""], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "zod": ["zod@3.25.76", "", {}, ""], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, ""], + } +} diff --git a/local-fs/package.json b/local-fs/package.json new file mode 100644 index 00000000..dcd2e453 --- /dev/null +++ b/local-fs/package.json @@ -0,0 +1,59 @@ +{ + "name": "@decocms/mcp-local-fs", + "version": "1.1.0", + "description": "MCP server that mounts any local filesystem path. Supports stdio (default), HTTP, and serve mode with deco link.", + "type": "module", + "main": "./dist/cli.js", + "bin": { + "mcp-local-fs": "./dist/cli.js", + "local-fs-serve": "./server/serve.ts" + }, + "files": [ + "dist", + "server", + "README.md" + ], + "scripts": { + "build": "tsc", + "dev": "bun run server/http.ts", + "dev:stdio": "bun run server/stdio.ts", + "serve": "bun run server/serve.ts", + "start": "node dist/cli.js", + "start:http": "node dist/cli.js --http", + "check": "tsc --noEmit", + "test": "bun test", + "test:watch": "bun test --watch", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.20.2", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "keywords": [ + "mcp", + "model-context-protocol", + "filesystem", + "local-fs", + "ai", + "claude", + "mesh", + "stdio" + ], + "repository": { + "type": "git", + "url": "https://github.com/decocms/mcps.git", + "directory": "local-fs" + }, + "author": "DecoCMS", + "license": "MIT", + "publishConfig": { + "access": "public" + } +} diff --git a/local-fs/server/cli.ts b/local-fs/server/cli.ts new file mode 100644 index 00000000..9347ebf0 --- /dev/null +++ b/local-fs/server/cli.ts @@ -0,0 +1,29 @@ +#!/usr/bin/env node +/** + * MCP Local FS - CLI Entry Point + * + * Unified CLI that supports both stdio (default) and http transports. + * + * Usage: + * npx @decocms/mcp-local-fs /path/to/mount # stdio mode (default) + * npx @decocms/mcp-local-fs --http /path/to/mount # http mode + * npx @decocms/mcp-local-fs --http --port 8080 # http mode with custom port + */ + +const args = process.argv.slice(2); + +// Check for --http flag +const httpIndex = args.indexOf("--http"); +const isHttpMode = httpIndex !== -1; + +if (isHttpMode) { + // Remove --http flag from args before passing to http module + args.splice(httpIndex, 1); + process.argv = [process.argv[0], process.argv[1], ...args]; + + // Dynamic import of http module + import("./http.js"); +} else { + // Default to stdio mode + import("./stdio.js"); +} diff --git a/local-fs/server/http.ts b/local-fs/server/http.ts new file mode 100644 index 00000000..157b4cbd --- /dev/null +++ b/local-fs/server/http.ts @@ -0,0 +1,335 @@ +#!/usr/bin/env node +/** + * MCP Local FS - HTTP Entry Point + * + * Usage: + * npx @decocms/mcp-local-fs --http --path /path/to/mount + * curl http://localhost:3456/mcp?path=/my/folder + * + * The path can be provided via: + * 1. Query string: ?path=/my/folder + * 2. --path CLI flag + * 3. MCP_LOCAL_FS_PATH environment variable + */ + +import { + createServer, + type IncomingMessage, + type ServerResponse, +} from "node:http"; +import { spawn } from "node:child_process"; +import { platform } from "node:os"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { LocalFileStorage } from "./storage.js"; +import { registerTools } from "./tools.js"; +import { resolve } from "node:path"; + +/** + * Copy text to clipboard (cross-platform) + */ +function copyToClipboard(text: string): Promise { + return new Promise((resolvePromise) => { + const os = platform(); + let cmd: string; + let args: string[]; + + if (os === "darwin") { + cmd = "pbcopy"; + args = []; + } else if (os === "win32") { + cmd = "clip"; + args = []; + } else { + // Linux - try xclip first, then xsel + cmd = "xclip"; + args = ["-selection", "clipboard"]; + } + + try { + const proc = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] }); + proc.stdin?.write(text); + proc.stdin?.end(); + proc.on("close", (code) => resolvePromise(code === 0)); + proc.on("error", () => resolvePromise(false)); + } catch { + resolvePromise(false); + } + }); +} + +/** + * Create an MCP server for a given filesystem path + */ +function createMcpServerForPath(rootPath: string): McpServer { + const storage = new LocalFileStorage(rootPath); + + const server = new McpServer({ + name: "local-fs", + version: "1.0.0", + }); + + // Register all tools from shared module + registerTools(server, storage); + + return server; +} + +// Parse CLI args for port and path +function getPort(): number { + const args = process.argv.slice(2); + for (let i = 0; i < args.length; i++) { + if (args[i] === "--port" || args[i] === "-p") { + const port = parseInt(args[i + 1], 10); + if (!isNaN(port)) return port; + } + } + return parseInt(process.env.PORT || "3456", 10); +} + +function getDefaultPath(): string { + const args = process.argv.slice(2); + + // Check for explicit --path flag + for (let i = 0; i < args.length; i++) { + if (args[i] === "--path" || args[i] === "-d") { + const path = args[i + 1]; + if (path && !path.startsWith("-")) return path; + } + } + + // Check for positional argument (skip flags and their values) + const skipNext = new Set(); + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + // Skip flag values + if (skipNext.has(i)) continue; + // Mark next arg to skip if this is a flag with value + if (arg === "--port" || arg === "-p" || arg === "--path" || arg === "-d") { + skipNext.add(i + 1); + continue; + } + // Skip flags + if (arg.startsWith("-")) continue; + // This is a positional argument - use it as path + return arg; + } + + return process.env.MCP_LOCAL_FS_PATH || process.cwd(); +} + +const port = getPort(); +const defaultPath = resolve(getDefaultPath()); + +// Session TTL in milliseconds (30 minutes) +const SESSION_TTL_MS = 30 * 60 * 1000; + +// Store active transports for session management with timestamps +const transports = new Map< + string, + { transport: StreamableHTTPServerTransport; lastAccess: number } +>(); + +// Cleanup stale sessions periodically (every 5 minutes) +const cleanupInterval = setInterval( + () => { + const now = Date.now(); + for (const [sessionId, session] of transports) { + if (now - session.lastAccess > SESSION_TTL_MS) { + transports.delete(sessionId); + console.log(`[mcp-local-fs] Session expired: ${sessionId}`); + } + } + }, + 5 * 60 * 1000, +); + +// Cleanup on process exit +process.on("SIGINT", () => { + clearInterval(cleanupInterval); + process.exit(0); +}); +process.on("SIGTERM", () => { + clearInterval(cleanupInterval); + process.exit(0); +}); + +// Create HTTP server +const httpServer = createServer( + async (req: IncomingMessage, res: ServerResponse) => { + try { + const url = new URL(req.url || "/", `http://localhost:${port}`); + + // CORS headers + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader( + "Access-Control-Allow-Methods", + "GET, POST, DELETE, OPTIONS", + ); + res.setHeader( + "Access-Control-Allow-Headers", + "Content-Type, mcp-session-id", + ); + + if (req.method === "OPTIONS") { + res.writeHead(204); + res.end(); + return; + } + + // Info endpoint + if (url.pathname === "/" && req.method === "GET") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + name: "mcp-local-fs", + version: "1.0.0", + description: "MCP server that mounts any local filesystem path", + endpoints: { + mcp: "/mcp?path=/your/path", + mcpWithPath: "/mcp/your/path", + health: "/health", + }, + defaultPath, + }), + ); + return; + } + + // Health check + if (url.pathname === "/health" && req.method === "GET") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "ok" })); + return; + } + + // MCP endpoint + if (url.pathname.startsWith("/mcp")) { + // Get path from query string or URL path + let mountPath = defaultPath; + const queryPath = url.searchParams.get("path"); + if (queryPath) { + mountPath = resolve(queryPath); + } else if ( + url.pathname !== "/mcp" && + url.pathname.startsWith("/mcp/") + ) { + const pathFromUrl = url.pathname.replace("/mcp/", ""); + mountPath = resolve("/" + decodeURIComponent(pathFromUrl)); + } + + console.log(`[mcp-local-fs] Request for path: ${mountPath}`); + + // Get or create session + const sessionId = req.headers["mcp-session-id"] as string | undefined; + + if (req.method === "POST") { + // Check for existing session + let session = sessionId ? transports.get(sessionId) : undefined; + + if (!session) { + // Create new transport and server for this session + const mcpServer = createMcpServerForPath(mountPath); + const newTransport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID(), + onsessioninitialized: (newSessionId) => { + transports.set(newSessionId, { + transport: newTransport, + lastAccess: Date.now(), + }); + console.log( + `[mcp-local-fs] Session initialized: ${newSessionId}`, + ); + }, + }); + + // Connect server to transport + await mcpServer.connect(newTransport); + + // Handle the request + await newTransport.handleRequest(req, res); + return; + } + + // Update last access time + session.lastAccess = Date.now(); + + // Handle the request + await session.transport.handleRequest(req, res); + return; + } + + if (req.method === "GET") { + // SSE connection for server-sent events + const session = sessionId ? transports.get(sessionId) : undefined; + if (session) { + session.lastAccess = Date.now(); + await session.transport.handleRequest(req, res); + return; + } + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "No session found" })); + return; + } + + if (req.method === "DELETE") { + // Session termination + const session = sessionId ? transports.get(sessionId) : undefined; + if (session) { + await session.transport.handleRequest(req, res); + transports.delete(sessionId!); + console.log(`[mcp-local-fs] Session terminated: ${sessionId}`); + return; + } + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Session not found" })); + return; + } + } + + // 404 for unknown routes + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Not found" })); + } catch (error) { + // Top-level error handler + console.error("[mcp-local-fs] Request error:", error); + if (!res.headersSent) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: "Internal server error", + message: error instanceof Error ? error.message : "Unknown error", + }), + ); + } + } + }, +); + +// Build the full MCP URL +const mcpUrl = `http://localhost:${port}/mcp${defaultPath}`; + +// Copy to clipboard and show startup banner +(async () => { + const copied = await copyToClipboard(mcpUrl); + + console.log(` +╔════════════════════════════════════════════════════════════╗ +║ MCP Local FS Server ║ +╠════════════════════════════════════════════════════════════╣ +║ HTTP server running on port ${port.toString().padEnd(27)}║ +║ Default path: ${defaultPath.slice(0, 41).padEnd(41)}║ +║ ║ +║ MCP URL (${copied ? "copied to clipboard ✓" : "copy this"}): +║ ${mcpUrl} +║ ║ +║ Endpoints: ║ +║ GET / Server info ║ +║ GET /health Health check ║ +║ POST /mcp MCP endpoint (use ?path=...) ║ +║ POST /mcp/* MCP endpoint with path in URL ║ +╚════════════════════════════════════════════════════════════╝ +`); +})(); + +httpServer.listen(port); diff --git a/local-fs/server/logger.ts b/local-fs/server/logger.ts new file mode 100644 index 00000000..a857aaf5 --- /dev/null +++ b/local-fs/server/logger.ts @@ -0,0 +1,168 @@ +/** + * MCP Local FS - Logger + * + * Nice formatted logging that goes to stderr (to not interfere with stdio protocol) + * but uses colors/formatting that indicate it's informational, not an error. + */ + +// ANSI color codes +const colors = { + reset: "\x1b[0m", + dim: "\x1b[2m", + bold: "\x1b[1m", + + // Foreground colors + cyan: "\x1b[36m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + gray: "\x1b[90m", + white: "\x1b[37m", +}; + +// Operation type colors +const opColors: Record = { + READ: colors.cyan, + WRITE: colors.green, + DELETE: colors.yellow, + MOVE: colors.magenta, + COPY: colors.blue, + MKDIR: colors.blue, + LIST: colors.gray, + STAT: colors.gray, + EDIT: colors.green, + SEARCH: colors.cyan, +}; + +function timestamp(): string { + const now = new Date(); + return `${colors.dim}${now.toLocaleTimeString("en-US", { hour12: false })}${colors.reset}`; +} + +function formatPath(path: string): string { + return `${colors.white}${path}${colors.reset}`; +} + +function formatOp(op: string): string { + const color = opColors[op] || colors.white; + return `${color}${colors.bold}${op.padEnd(6)}${colors.reset}`; +} + +function formatSize(bytes: number): string { + const units = ["B", "KB", "MB", "GB"]; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${colors.dim}(${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]})${colors.reset}`; +} + +const prefix = `${colors.cyan}◆${colors.reset}`; + +/** + * Log a file operation + */ +export function logOp( + op: string, + path: string, + extra?: { + size?: number; + to?: string; + count?: number; + recursive?: boolean; + error?: string; + }, +): void { + let msg = `${prefix} ${timestamp()} ${formatOp(op)} ${formatPath(path)}`; + + if (extra?.to) { + msg += ` ${colors.dim}→${colors.reset} ${formatPath(extra.to)}`; + } + + if (extra?.size !== undefined) { + msg += ` ${formatSize(extra.size)}`; + } + + if (extra?.count !== undefined) { + const recursiveLabel = extra.recursive ? " recursive" : ""; + msg += ` ${colors.dim}(${extra.count}${recursiveLabel} items)${colors.reset}`; + } + + if (extra?.error) { + msg += ` ${colors.yellow}[${extra.error}]${colors.reset}`; + } + + console.error(msg); +} + +/** + * Log server startup + */ +export function logStart(rootPath: string): void { + console.error( + `\n${prefix} ${colors.cyan}${colors.bold}mcp-local-fs${colors.reset} ${colors.dim}started${colors.reset}`, + ); + console.error( + `${prefix} ${colors.dim}root:${colors.reset} ${colors.white}${rootPath}${colors.reset}\n`, + ); +} + +/** + * Log an error (still uses red, but with the prefix) + */ +export function logError(op: string, path: string, error: Error): void { + console.error( + `${prefix} ${timestamp()} ${colors.yellow}${colors.bold}ERR${colors.reset} ${formatOp(op)} ${formatPath(path)} ${colors.dim}${error.message}${colors.reset}`, + ); +} + +/** + * Log a tool call + */ +export function logTool( + toolName: string, + args: Record, + result?: { isError?: boolean }, +): void { + const argsStr = formatArgs(args); + const status = result?.isError + ? `${colors.yellow}✗${colors.reset}` + : `${colors.green}✓${colors.reset}`; + + if (result) { + console.error( + `${prefix} ${timestamp()} ${colors.magenta}${colors.bold}TOOL${colors.reset} ${colors.white}${toolName}${colors.reset}${argsStr} ${status}`, + ); + } else { + console.error( + `${prefix} ${timestamp()} ${colors.magenta}${colors.bold}TOOL${colors.reset} ${colors.white}${toolName}${colors.reset}${argsStr}`, + ); + } +} + +function formatArgs(args: Record): string { + const entries = Object.entries(args); + if (entries.length === 0) return ""; + + const parts = entries.map(([key, value]) => { + let valStr: string; + if (typeof value === "string") { + // Truncate long strings + valStr = value.length > 50 ? `"${value.slice(0, 47)}..."` : `"${value}"`; + } else if (Array.isArray(value)) { + valStr = `[${value.length} items]`; + } else if (typeof value === "object" && value !== null) { + valStr = "{...}"; + } else { + valStr = String(value); + } + return `${colors.dim}${key}=${colors.reset}${valStr}`; + }); + + return ` ${parts.join(" ")}`; +} diff --git a/local-fs/server/mcp.test.ts b/local-fs/server/mcp.test.ts new file mode 100644 index 00000000..056a8909 --- /dev/null +++ b/local-fs/server/mcp.test.ts @@ -0,0 +1,631 @@ +/** + * MCP Server Integration Tests + * + * Tests for the MCP server tools and protocol integration. + * Uses the actual registerTools function to test the real implementation. + */ + +import { + describe, + test, + expect, + beforeAll, + afterAll, + beforeEach, +} from "bun:test"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LocalFileStorage } from "./storage.js"; +import { registerTools } from "./tools.js"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +describe("MCP Server Integration", () => { + let tempDir: string; + let storage: LocalFileStorage; + let server: McpServer; + let client: Client; + + beforeAll(async () => { + // Create temp directory + tempDir = await mkdtemp(join(tmpdir(), "mcp-server-test-")); + storage = new LocalFileStorage(tempDir); + + // Create MCP server with shared tools + server = new McpServer({ + name: "local-fs", + version: "1.0.0", + }); + registerTools(server, storage); + + // Create in-memory transport pair + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + // Connect server and client + await server.connect(serverTransport); + + client = new Client({ + name: "test-client", + version: "1.0.0", + }); + await client.connect(clientTransport); + }); + + afterAll(async () => { + await client.close(); + await server.close(); + await rm(tempDir, { recursive: true, force: true }); + }); + + beforeEach(async () => { + // Clean the temp directory before each test + const entries = await storage.list(""); + for (const entry of entries) { + await rm(join(tempDir, entry.path), { recursive: true, force: true }); + } + }); + + describe("tools/list", () => { + test("should list all official MCP filesystem tools", async () => { + const result = await client.listTools(); + + expect(result.tools.length).toBeGreaterThan(0); + + const toolNames = result.tools.map((t) => t.name); + + // Official MCP filesystem tools + expect(toolNames).toContain("read_file"); + expect(toolNames).toContain("read_text_file"); + expect(toolNames).toContain("read_media_file"); + expect(toolNames).toContain("read_multiple_files"); + expect(toolNames).toContain("write_file"); + expect(toolNames).toContain("edit_file"); + expect(toolNames).toContain("create_directory"); + expect(toolNames).toContain("list_directory"); + expect(toolNames).toContain("list_directory_with_sizes"); + expect(toolNames).toContain("directory_tree"); + expect(toolNames).toContain("move_file"); + expect(toolNames).toContain("search_files"); + expect(toolNames).toContain("get_file_info"); + expect(toolNames).toContain("list_allowed_directories"); + + // Additional tools + expect(toolNames).toContain("delete_file"); + expect(toolNames).toContain("copy_file"); + + // Mesh collection bindings + expect(toolNames).toContain("COLLECTION_FILES_LIST"); + expect(toolNames).toContain("COLLECTION_FILES_GET"); + expect(toolNames).toContain("COLLECTION_FOLDERS_LIST"); + expect(toolNames).toContain("COLLECTION_FOLDERS_GET"); + }); + + test("each tool should have a description", async () => { + const result = await client.listTools(); + + for (const tool of result.tools) { + expect(tool.description).toBeDefined(); + expect(tool.description!.length).toBeGreaterThan(0); + } + }); + }); + + describe("write_file tool", () => { + test("should write a file successfully", async () => { + const result = await client.callTool({ + name: "write_file", + arguments: { + path: "test-write.txt", + content: "Hello from MCP!", + }, + }); + + expect(result.isError).toBeFalsy(); + expect(result.content).toBeDefined(); + + // Verify file was written + const readResult = await storage.read("test-write.txt"); + expect(readResult.content).toBe("Hello from MCP!"); + }); + + test("should create nested directories", async () => { + const result = await client.callTool({ + name: "write_file", + arguments: { + path: "nested/path/file.txt", + content: "Nested content", + }, + }); + + expect(result.isError).toBeFalsy(); + + const readResult = await storage.read("nested/path/file.txt"); + expect(readResult.content).toBe("Nested content"); + }); + }); + + describe("read_text_file tool", () => { + test("should read a file successfully", async () => { + await storage.write("read-test.txt", "Content to read"); + + const result = await client.callTool({ + name: "read_text_file", + arguments: { + path: "read-test.txt", + }, + }); + + expect(result.isError).toBeFalsy(); + + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + expect(textContent[0].text).toBe("Content to read"); + }); + + test("should return error for non-existent file", async () => { + const result = await client.callTool({ + name: "read_text_file", + arguments: { + path: "does-not-exist.txt", + }, + }); + + expect(result.isError).toBe(true); + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + expect(textContent[0].text).toContain("Error:"); + }); + + test("should support head parameter", async () => { + await storage.write( + "lines.txt", + "Line 1\nLine 2\nLine 3\nLine 4\nLine 5", + ); + + const result = await client.callTool({ + name: "read_text_file", + arguments: { + path: "lines.txt", + head: 2, + }, + }); + + expect(result.isError).toBeFalsy(); + + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + expect(textContent[0].text).toBe("Line 1\nLine 2"); + }); + + test("should support tail parameter", async () => { + await storage.write( + "lines.txt", + "Line 1\nLine 2\nLine 3\nLine 4\nLine 5", + ); + + const result = await client.callTool({ + name: "read_text_file", + arguments: { + path: "lines.txt", + tail: 2, + }, + }); + + expect(result.isError).toBeFalsy(); + + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + expect(textContent[0].text).toBe("Line 4\nLine 5"); + }); + }); + + describe("read_multiple_files tool", () => { + test("should read multiple files at once", async () => { + await storage.write("file1.txt", "Content 1"); + await storage.write("file2.txt", "Content 2"); + + const result = await client.callTool({ + name: "read_multiple_files", + arguments: { + paths: ["file1.txt", "file2.txt"], + }, + }); + + expect(result.isError).toBeFalsy(); + + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + expect(textContent[0].text).toContain("file1.txt:"); + expect(textContent[0].text).toContain("Content 1"); + expect(textContent[0].text).toContain("file2.txt:"); + expect(textContent[0].text).toContain("Content 2"); + }); + }); + + describe("delete_file tool", () => { + test("should delete a file", async () => { + await storage.write("to-delete.txt", "Delete me"); + + const result = await client.callTool({ + name: "delete_file", + arguments: { + path: "to-delete.txt", + }, + }); + + expect(result.isError).toBeFalsy(); + + // Verify file was deleted + await expect(storage.getMetadata("to-delete.txt")).rejects.toThrow(); + }); + + test("should delete directory recursively", async () => { + await storage.write("dir-delete/file.txt", "content"); + + const result = await client.callTool({ + name: "delete_file", + arguments: { + path: "dir-delete", + recursive: true, + }, + }); + + expect(result.isError).toBeFalsy(); + + await expect(storage.getMetadata("dir-delete")).rejects.toThrow(); + }); + }); + + describe("list_directory tool", () => { + test("should list files and directories", async () => { + await storage.write("file.txt", "content"); + await storage.mkdir("subdir"); + + const result = await client.callTool({ + name: "list_directory", + arguments: { + path: "", + }, + }); + + expect(result.isError).toBeFalsy(); + + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + expect(textContent[0].text).toContain("[FILE] file.txt"); + expect(textContent[0].text).toContain("[DIR] subdir"); + }); + }); + + describe("create_directory tool", () => { + test("should create a directory", async () => { + const result = await client.callTool({ + name: "create_directory", + arguments: { + path: "new-dir", + }, + }); + + expect(result.isError).toBeFalsy(); + + const meta = await storage.getMetadata("new-dir"); + expect(meta.isDirectory).toBe(true); + }); + + test("should create nested directories", async () => { + const result = await client.callTool({ + name: "create_directory", + arguments: { + path: "deep/nested/dir", + }, + }); + + expect(result.isError).toBeFalsy(); + + const meta = await storage.getMetadata("deep/nested/dir"); + expect(meta.isDirectory).toBe(true); + }); + }); + + describe("move_file tool", () => { + test("should move a file", async () => { + await storage.write("original.txt", "content"); + + const result = await client.callTool({ + name: "move_file", + arguments: { + source: "original.txt", + destination: "moved.txt", + }, + }); + + expect(result.isError).toBeFalsy(); + + await expect(storage.getMetadata("original.txt")).rejects.toThrow(); + const content = await storage.read("moved.txt"); + expect(content.content).toBe("content"); + }); + }); + + describe("copy_file tool", () => { + test("should copy a file", async () => { + await storage.write("original.txt", "content"); + + const result = await client.callTool({ + name: "copy_file", + arguments: { + source: "original.txt", + destination: "copy.txt", + }, + }); + + expect(result.isError).toBeFalsy(); + + const original = await storage.read("original.txt"); + const copy = await storage.read("copy.txt"); + expect(original.content).toBe("content"); + expect(copy.content).toBe("content"); + }); + }); + + describe("get_file_info tool", () => { + test("should return file metadata", async () => { + await storage.write("info-test.txt", "some content"); + + const result = await client.callTool({ + name: "get_file_info", + arguments: { + path: "info-test.txt", + }, + }); + + expect(result.isError).toBeFalsy(); + + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + expect(textContent[0].text).toContain("type: file"); + expect(textContent[0].text).toContain("size:"); + }); + }); + + describe("search_files tool", () => { + test("should find files matching pattern", async () => { + await storage.write("test.txt", "content"); + await storage.write("test.js", "content"); + await storage.write("other.md", "content"); + + const result = await client.callTool({ + name: "search_files", + arguments: { + path: "", + pattern: "*.txt", + }, + }); + + expect(result.isError).toBeFalsy(); + + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + expect(textContent[0].text).toContain("test.txt"); + expect(textContent[0].text).not.toContain("test.js"); + }); + }); + + describe("edit_file tool", () => { + test("should edit file with search and replace", async () => { + await storage.write("edit-test.txt", "Hello World"); + + const result = await client.callTool({ + name: "edit_file", + arguments: { + path: "edit-test.txt", + edits: [{ oldText: "World", newText: "MCP" }], + }, + }); + + expect(result.isError).toBeFalsy(); + + const content = await storage.read("edit-test.txt"); + expect(content.content).toBe("Hello MCP"); + }); + + test("should support dry run", async () => { + await storage.write("edit-test.txt", "Hello World"); + + const result = await client.callTool({ + name: "edit_file", + arguments: { + path: "edit-test.txt", + edits: [{ oldText: "World", newText: "MCP" }], + dryRun: true, + }, + }); + + expect(result.isError).toBeFalsy(); + + // File should not be changed + const content = await storage.read("edit-test.txt"); + expect(content.content).toBe("Hello World"); + + // Response should include diff preview + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + expect(textContent[0].text).toContain("Dry run"); + }); + }); + + describe("COLLECTION_FILES_LIST tool", () => { + test("should list files in root", async () => { + await storage.write("file1.txt", "content1"); + await storage.write("file2.txt", "content2"); + + const result = await client.callTool({ + name: "COLLECTION_FILES_LIST", + arguments: {}, + }); + + expect(result.isError).toBeFalsy(); + + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + const parsed = JSON.parse(textContent[0].text); + + expect(parsed.items.length).toBe(2); + expect(parsed.totalCount).toBe(2); + }); + + test("should list files recursively", async () => { + await storage.write("root.txt", "root"); + await storage.write("sub/nested.txt", "nested"); + + const result = await client.callTool({ + name: "COLLECTION_FILES_LIST", + arguments: { + recursive: true, + }, + }); + + expect(result.isError).toBeFalsy(); + + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + const parsed = JSON.parse(textContent[0].text); + + expect(parsed.items.length).toBe(2); + const paths = parsed.items.map((i: { path: string }) => i.path); + expect(paths).toContain("root.txt"); + expect(paths).toContain("sub/nested.txt"); + }); + + test("should respect limit parameter", async () => { + await storage.write("file1.txt", "1"); + await storage.write("file2.txt", "2"); + await storage.write("file3.txt", "3"); + + const result = await client.callTool({ + name: "COLLECTION_FILES_LIST", + arguments: { + limit: 2, + }, + }); + + expect(result.isError).toBeFalsy(); + + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + const parsed = JSON.parse(textContent[0].text); + + expect(parsed.items.length).toBe(2); + expect(parsed.totalCount).toBe(3); + expect(parsed.hasMore).toBe(true); + }); + }); + + describe("COLLECTION_FOLDERS_LIST tool", () => { + test("should list folders", async () => { + await storage.mkdir("folder1"); + await storage.mkdir("folder2"); + await storage.write("file.txt", "content"); + + const result = await client.callTool({ + name: "COLLECTION_FOLDERS_LIST", + arguments: {}, + }); + + expect(result.isError).toBeFalsy(); + + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + const parsed = JSON.parse(textContent[0].text); + + expect(parsed.items.length).toBe(2); + expect( + parsed.items.every((i: { isDirectory: boolean }) => i.isDirectory), + ).toBe(true); + }); + }); + + describe("COLLECTION_FILES_GET tool", () => { + test("should return file metadata and content", async () => { + await storage.write("get-test.txt", "Hello from GET test!"); + + const result = await client.callTool({ + name: "COLLECTION_FILES_GET", + arguments: { id: "get-test.txt" }, + }); + + expect(result.isError).toBeFalsy(); + + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + const parsed = JSON.parse(textContent[0].text); + + expect(parsed.item).toBeDefined(); + expect(parsed.item.path).toBe("get-test.txt"); + expect(parsed.item.content).toBe("Hello from GET test!"); + expect(parsed.item.isDirectory).toBe(false); + }); + }); + + describe("list_allowed_directories tool", () => { + test("should return the root directory", async () => { + const result = await client.callTool({ + name: "list_allowed_directories", + arguments: {}, + }); + + expect(result.isError).toBeFalsy(); + + const textContent = result.content as Array<{ + type: string; + text: string; + }>; + expect(textContent[0].text).toContain(tempDir); + }); + }); + + describe("error handling", () => { + test("should handle invalid file paths gracefully", async () => { + const result = await client.callTool({ + name: "read_text_file", + arguments: { + path: "", + }, + }); + + // Should return an error response, not throw + expect(result.isError).toBe(true); + }); + }); +}); diff --git a/local-fs/server/serve.ts b/local-fs/server/serve.ts new file mode 100755 index 00000000..16aa2115 --- /dev/null +++ b/local-fs/server/serve.ts @@ -0,0 +1,215 @@ +#!/usr/bin/env bun +/** + * MCP Local FS - Serve & Link + * + * Exposes the current directory (or specified path) via deco link + * and provides a ready-to-add MCP URL for mesh. + * + * Usage: + * bunx @decocms/mcp-local-fs serve # Current directory + * bunx @decocms/mcp-local-fs serve /my/folder # Specific folder + * bunx @decocms/mcp-local-fs serve --port 8080 # Custom port + */ + +import { spawn } from "node:child_process"; +import { platform } from "node:os"; +import { resolve } from "node:path"; +import { existsSync } from "node:fs"; + +const PORT = parseInt(process.env.PORT || "3456", 10); + +/** + * Parse CLI arguments + */ +function parseArgs(): { path: string; port: number } { + const args = process.argv.slice(2); + let path = process.cwd(); + let port = PORT; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + // Skip "serve" command itself + if (arg === "serve") continue; + + // Port flag + if (arg === "--port" || arg === "-p") { + const p = parseInt(args[++i], 10); + if (!isNaN(p)) port = p; + continue; + } + + // Skip other flags + if (arg.startsWith("-")) continue; + + // Positional argument = path + path = resolve(arg); + } + + return { path, port }; +} + +/** + * Copy text to clipboard + */ +function copyToClipboard(text: string): Promise { + return new Promise((resolvePromise) => { + const os = platform(); + let cmd: string; + let args: string[]; + + if (os === "darwin") { + cmd = "pbcopy"; + args = []; + } else if (os === "win32") { + cmd = "clip"; + args = []; + } else { + cmd = "xclip"; + args = ["-selection", "clipboard"]; + } + + try { + const proc = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] }); + proc.stdin?.write(text); + proc.stdin?.end(); + proc.on("close", (code) => resolvePromise(code === 0)); + proc.on("error", () => resolvePromise(false)); + } catch { + resolvePromise(false); + } + }); +} + +const { path, port } = parseArgs(); + +// Validate path exists +if (!existsSync(path)) { + console.error(`\n❌ Path does not exist: ${path}\n`); + process.exit(1); +} + +// URL-encode the path for the MCP endpoint +const encodedPath = encodeURIComponent(path); + +// Track if we've shown the URL already +let publicUrl = ""; + +/** + * Show the MCP URL banner when we detect the tunnel URL + */ +async function showMcpUrl(tunnelUrl: string) { + if (publicUrl) return; // Already shown + publicUrl = tunnelUrl; + + // Build the full MCP URL with path + const mcpUrl = `${publicUrl}/mcp?path=${encodedPath}`; + + const copied = await copyToClipboard(mcpUrl); + + console.log(` + +╔══════════════════════════════════════════════════════════════════════════════════════════════════╗ +║ ✅ Ready to Use! ║ +╠══════════════════════════════════════════════════════════════════════════════════════════════════╣ +║ ║ +║ Add this MCP URL to your Deco Mesh: ║ +║ ║ +║ ${mcpUrl} +║ ║ +║ ${copied ? "📋 Copied to clipboard!" : "Copy the URL above"} ║ +║ ║ +║ Steps: ║ +║ 1. Open mesh-admin.decocms.com ║ +║ 2. Go to Connections → Add Custom MCP ║ +║ 3. Paste the URL above ║ +║ ║ +╚══════════════════════════════════════════════════════════════════════════════════════════════════╝ +`); +} + +/** + * Check output for tunnel URL + */ +function checkForTunnelUrl(output: string) { + // Match URLs like https://localhost-xxx.deco.host or https://xxx.deco.site + const urlMatch = output.match(/https:\/\/[^\s()"']+\.deco\.(site|host)/); + if (urlMatch) { + const url = urlMatch[0].replace(/[()]/g, ""); + showMcpUrl(url); + } +} + +console.log(` +╔════════════════════════════════════════════════════════════╗ +║ MCP Local FS - Serve & Link ║ +╠════════════════════════════════════════════════════════════╣ +║ 📁 Serving: ${path.slice(0, 43).padEnd(43)}║ +║ 🔌 Port: ${port.toString().padEnd(47)}║ +║ ║ +║ ⚠️ Note: Only ONE deco link tunnel per machine. ║ +║ Stop other 'serve' commands first. ║ +╚════════════════════════════════════════════════════════════╝ + +Starting server and tunnel... +`); + +// 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"); + +// Start the HTTP server in background +const serverProcess = spawn( + "bun", + ["run", httpScript, "--port", port.toString(), "--path", path], + { + stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, PORT: port.toString() }, + }, +); + +serverProcess.stderr?.on("data", (data) => { + const output = data.toString().trim(); + if (output && !output.includes("MCP Local FS")) { + console.error(`[server] ${output}`); + } +}); + +// Wait for server to start +await new Promise((r) => setTimeout(r, 1500)); + +// Run deco link to get the public URL +const decoLink = spawn("deco", ["link", "-p", port.toString()], { + stdio: ["inherit", "pipe", "pipe"], +}); + +decoLink.stdout?.on("data", (data) => { + const output = data.toString(); + process.stdout.write(output); + checkForTunnelUrl(output); +}); + +decoLink.stderr?.on("data", (data) => { + const output = data.toString(); + process.stderr.write(output); + checkForTunnelUrl(output); +}); + +// Handle exit +process.on("SIGINT", () => { + serverProcess.kill(); + decoLink.kill(); + process.exit(0); +}); + +process.on("SIGTERM", () => { + serverProcess.kill(); + decoLink.kill(); + process.exit(0); +}); + +// Wait for deco link to exit +decoLink.on("close", (code) => { + serverProcess.kill(); + process.exit(code || 0); +}); diff --git a/local-fs/server/stdio.ts b/local-fs/server/stdio.ts new file mode 100644 index 00000000..76ddc6a0 --- /dev/null +++ b/local-fs/server/stdio.ts @@ -0,0 +1,82 @@ +#!/usr/bin/env node +/** + * MCP Local FS - Stdio Entry Point + * + * This is the main entry point for running the MCP server via stdio, + * which is the standard transport for CLI-based MCP servers. + * + * Usage: + * npx @decocms/mcp-local-fs /path/to/mount + * npx @decocms/mcp-local-fs --path /path/to/mount + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { LocalFileStorage } from "./storage.js"; +import { registerTools } from "./tools.js"; +import { logStart } from "./logger.js"; +import { resolve } from "node:path"; + +/** + * Parse CLI arguments to get the path to mount + */ +function getPathFromArgs(): string { + const args = process.argv.slice(2); + + // Check for --path flag + for (let i = 0; i < args.length; i++) { + if (args[i] === "--path" || args[i] === "-p") { + const path = args[i + 1]; + if (path && !path.startsWith("-")) { + return path; + } + } + } + + // Check for positional argument (first non-flag argument) + for (const arg of args) { + if (!arg.startsWith("-")) { + return arg; + } + } + + // Check environment variable + if (process.env.MCP_LOCAL_FS_PATH) { + return process.env.MCP_LOCAL_FS_PATH; + } + + // Default to current working directory + return process.cwd(); +} + +/** + * Create and start the MCP server with stdio transport + */ +async function main() { + const mountPath = getPathFromArgs(); + const resolvedPath = resolve(mountPath); + + // Create storage instance + const storage = new LocalFileStorage(resolvedPath); + + // Create MCP server + const server = new McpServer({ + name: "local-fs", + version: "1.0.0", + }); + + // Register all tools + registerTools(server, storage); + + // Connect to stdio transport + const transport = new StdioServerTransport(); + await server.connect(transport); + + // Log startup (goes to stderr, nicely formatted) + logStart(resolvedPath); +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/local-fs/server/storage.test.ts b/local-fs/server/storage.test.ts new file mode 100644 index 00000000..cb73b8fb --- /dev/null +++ b/local-fs/server/storage.test.ts @@ -0,0 +1,525 @@ +/** + * Storage Layer Tests + * + * Tests for the LocalFileStorage class - file system operations. + */ + +import { + describe, + test, + expect, + beforeAll, + afterAll, + beforeEach, +} from "bun:test"; +import { LocalFileStorage, getExtensionFromMimeType } from "./storage.js"; +import { Readable } from "node:stream"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +describe("LocalFileStorage", () => { + let tempDir: string; + let storage: LocalFileStorage; + + beforeAll(async () => { + // Create a temp directory for tests + tempDir = await mkdtemp(join(tmpdir(), "mcp-local-fs-test-")); + storage = new LocalFileStorage(tempDir); + }); + + afterAll(async () => { + // Clean up temp directory + await rm(tempDir, { recursive: true, force: true }); + }); + + beforeEach(async () => { + // Clean the temp directory before each test + const entries = await storage.list(""); + for (const entry of entries) { + await rm(join(tempDir, entry.path), { recursive: true, force: true }); + } + }); + + describe("root property", () => { + test("should return the resolved root directory", () => { + expect(storage.root).toBe(tempDir); + }); + }); + + describe("write and read", () => { + test("should write and read a text file", async () => { + const content = "Hello, World!"; + await storage.write("test.txt", content); + + const result = await storage.read("test.txt"); + expect(result.content).toBe(content); + expect(result.metadata.path).toBe("test.txt"); + expect(result.metadata.mimeType).toBe("text/plain"); + }); + + test("should write and read a file with utf-8 encoding", async () => { + const content = "こんにちは世界 🌍"; + await storage.write("unicode.txt", content, { encoding: "utf-8" }); + + const result = await storage.read("unicode.txt", "utf-8"); + expect(result.content).toBe(content); + }); + + test("should write and read a file with base64 encoding", async () => { + const originalContent = "Binary test content"; + const base64Content = Buffer.from(originalContent).toString("base64"); + + await storage.write("binary.bin", base64Content, { encoding: "base64" }); + + const result = await storage.read("binary.bin", "base64"); + const decodedContent = Buffer.from(result.content, "base64").toString( + "utf-8", + ); + expect(decodedContent).toBe(originalContent); + }); + + test("should create parent directories when writing", async () => { + const content = "Nested file"; + await storage.write("nested/deep/file.txt", content, { + createParents: true, + }); + + const result = await storage.read("nested/deep/file.txt"); + expect(result.content).toBe(content); + }); + + test("should fail to overwrite when overwrite is false", async () => { + await storage.write("existing.txt", "original"); + + await expect( + storage.write("existing.txt", "new content", { overwrite: false }), + ).rejects.toThrow("File already exists"); + }); + + test("should overwrite when overwrite is true", async () => { + await storage.write("overwrite.txt", "original"); + await storage.write("overwrite.txt", "updated", { overwrite: true }); + + const result = await storage.read("overwrite.txt"); + expect(result.content).toBe("updated"); + }); + }); + + describe("getMetadata", () => { + test("should return metadata for a file", async () => { + await storage.write("meta-test.txt", "content"); + + const metadata = await storage.getMetadata("meta-test.txt"); + expect(metadata.id).toBe("meta-test.txt"); + expect(metadata.title).toBe("meta-test.txt"); + expect(metadata.isDirectory).toBe(false); + expect(metadata.mimeType).toBe("text/plain"); + expect(metadata.size).toBeGreaterThan(0); + expect(metadata.created_at).toBeDefined(); + expect(metadata.updated_at).toBeDefined(); + }); + + test("should return metadata for a directory", async () => { + await storage.mkdir("test-dir"); + + const metadata = await storage.getMetadata("test-dir"); + expect(metadata.isDirectory).toBe(true); + expect(metadata.mimeType).toBe("inode/directory"); + }); + + test("should throw for non-existent path", async () => { + await expect(storage.getMetadata("does-not-exist.txt")).rejects.toThrow(); + }); + }); + + describe("list", () => { + test("should list files in root directory", async () => { + await storage.write("file1.txt", "content1"); + await storage.write("file2.txt", "content2"); + + const items = await storage.list(""); + expect(items.length).toBe(2); + expect(items.map((i) => i.title)).toContain("file1.txt"); + expect(items.map((i) => i.title)).toContain("file2.txt"); + }); + + test("should list files in subdirectory", async () => { + await storage.mkdir("subdir"); + await storage.write("subdir/nested.txt", "nested content"); + + const items = await storage.list("subdir"); + expect(items.length).toBe(1); + expect(items[0].title).toBe("subdir/nested.txt"); + }); + + test("should list recursively when recursive=true", async () => { + await storage.write("root.txt", "root"); + await storage.write("level1/file1.txt", "level1"); + await storage.write("level1/level2/file2.txt", "level2"); + + const items = await storage.list("", { recursive: true }); + const paths = items.map((i) => i.path); + + expect(paths).toContain("root.txt"); + expect(paths).toContain("level1/file1.txt"); + expect(paths).toContain("level1/level2/file2.txt"); + }); + + test("should filter to files only when filesOnly=true", async () => { + await storage.mkdir("dir-only"); + await storage.write("file-only.txt", "content"); + + const items = await storage.list("", { filesOnly: true }); + expect(items.every((i) => !i.isDirectory)).toBe(true); + expect(items.map((i) => i.title)).toContain("file-only.txt"); + }); + + test("should return empty array for non-existent directory", async () => { + const items = await storage.list("non-existent"); + expect(items).toEqual([]); + }); + + test("should skip hidden files (starting with .)", async () => { + await writeFile(join(tempDir, ".hidden"), "hidden content"); + await storage.write("visible.txt", "visible content"); + + const items = await storage.list(""); + expect(items.map((i) => i.title)).not.toContain(".hidden"); + expect(items.map((i) => i.title)).toContain("visible.txt"); + }); + }); + + describe("mkdir", () => { + test("should create a directory", async () => { + const result = await storage.mkdir("new-dir"); + + expect(result.folder.isDirectory).toBe(true); + expect(result.folder.path).toBe("new-dir"); + }); + + test("should create nested directories with recursive=true", async () => { + const result = await storage.mkdir("a/b/c", true); + + expect(result.folder.path).toBe("a/b/c"); + + const metadata = await storage.getMetadata("a/b/c"); + expect(metadata.isDirectory).toBe(true); + }); + }); + + describe("delete", () => { + test("should delete a file", async () => { + await storage.write("to-delete.txt", "content"); + const result = await storage.delete("to-delete.txt"); + + expect(result.success).toBe(true); + await expect(storage.getMetadata("to-delete.txt")).rejects.toThrow(); + }); + + test("should delete an empty directory", async () => { + await storage.mkdir("empty-dir"); + const result = await storage.delete("empty-dir", true); + + expect(result.success).toBe(true); + }); + + test("should delete directory recursively", async () => { + await storage.write("dir-to-delete/file.txt", "content"); + const result = await storage.delete("dir-to-delete", true); + + expect(result.success).toBe(true); + await expect(storage.getMetadata("dir-to-delete")).rejects.toThrow(); + }); + + test("should fail to delete non-empty directory without recursive flag", async () => { + await storage.write("non-empty/file.txt", "content"); + + await expect(storage.delete("non-empty", false)).rejects.toThrow(); + }); + }); + + describe("move", () => { + test("should move a file", async () => { + await storage.write("source.txt", "content"); + const result = await storage.move("source.txt", "destination.txt"); + + expect(result.file.path).toBe("destination.txt"); + await expect(storage.getMetadata("source.txt")).rejects.toThrow(); + + const content = await storage.read("destination.txt"); + expect(content.content).toBe("content"); + }); + + test("should move a file to a subdirectory", async () => { + await storage.write("move-me.txt", "content"); + await storage.mkdir("target-dir"); + await storage.move("move-me.txt", "target-dir/moved.txt"); + + const content = await storage.read("target-dir/moved.txt"); + expect(content.content).toBe("content"); + }); + + test("should fail to overwrite without overwrite flag", async () => { + await storage.write("existing-dest.txt", "existing"); + await storage.write("new-source.txt", "new"); + + await expect( + storage.move("new-source.txt", "existing-dest.txt", false), + ).rejects.toThrow("Destination already exists"); + }); + + test("should overwrite with overwrite flag", async () => { + await storage.write("old.txt", "old content"); + await storage.write("new.txt", "new content"); + await storage.move("new.txt", "old.txt", true); + + const result = await storage.read("old.txt"); + expect(result.content).toBe("new content"); + }); + }); + + describe("copy", () => { + test("should copy a file", async () => { + await storage.write("original.txt", "content"); + const result = await storage.copy("original.txt", "copied.txt"); + + expect(result.file.path).toBe("copied.txt"); + + // Both files should exist + const original = await storage.read("original.txt"); + const copied = await storage.read("copied.txt"); + expect(original.content).toBe("content"); + expect(copied.content).toBe("content"); + }); + + test("should fail to overwrite without overwrite flag", async () => { + await storage.write("src.txt", "source"); + await storage.write("dst.txt", "destination"); + + await expect(storage.copy("src.txt", "dst.txt", false)).rejects.toThrow( + "Destination already exists", + ); + }); + + test("should overwrite with overwrite flag", async () => { + await storage.write("src.txt", "source content"); + await storage.write("dst.txt", "destination content"); + await storage.copy("src.txt", "dst.txt", true); + + const result = await storage.read("dst.txt"); + expect(result.content).toBe("source content"); + }); + }); + + describe("path sanitization", () => { + test("should prevent path traversal with ..", async () => { + await storage.write("safe.txt", "safe content"); + + // Attempting to traverse should be sanitized + const result = await storage.read("../safe.txt"); + // This should still find the file since .. is stripped + expect(result.content).toBe("safe content"); + }); + + test("should handle leading slashes", async () => { + await storage.write("leading-slash.txt", "content"); + + const result = await storage.read("/leading-slash.txt"); + expect(result.content).toBe("content"); + }); + }); + + describe("path normalization (stripping root prefix)", () => { + test("should strip root directory prefix from path", async () => { + await storage.write("normalize-test.txt", "normalized content"); + + // AI agents sometimes pass the full path including root + const fullPath = `${tempDir}/normalize-test.txt`; + const result = await storage.read(fullPath); + expect(result.content).toBe("normalized content"); + }); + + test("should strip root with colon separator", async () => { + await storage.write("colon-test.txt", "colon content"); + + // Some tools format paths as "root:filename" + const colonPath = `${tempDir}:colon-test.txt`; + const result = await storage.read(colonPath); + expect(result.content).toBe("colon content"); + }); + + test("normalizePath should return relative path", () => { + const relPath = storage.normalizePath(`${tempDir}/some/file.txt`); + expect(relPath).toBe("some/file.txt"); + }); + + test("normalizePath should handle already-relative paths", () => { + const relPath = storage.normalizePath("some/file.txt"); + expect(relPath).toBe("some/file.txt"); + }); + + test("normalizePath should handle colon separator", () => { + const relPath = storage.normalizePath(`${tempDir}:file.txt`); + expect(relPath).toBe("file.txt"); + }); + + test("normalizePath should strip leading slashes", () => { + const relPath = storage.normalizePath("/file.txt"); + expect(relPath).toBe("file.txt"); + }); + + test("normalizePath should NOT match paths that share prefix but are not inside root", () => { + // If rootDir is /tmp/root, a path like /tmp/rootEvil/file.txt should NOT + // be treated as inside the root directory + const relPath = storage.normalizePath(`${tempDir}Evil/file.txt`); + // Should return the full path unchanged (minus leading slash stripping) + expect(relPath).not.toBe("Evil/file.txt"); + // Instead it should be the original path with leading slash stripped + expect(relPath).toContain("Evil/file.txt"); + }); + }); + + describe("MIME type detection", () => { + const testCases = [ + { ext: ".txt", expected: "text/plain" }, + { ext: ".json", expected: "application/json" }, + { ext: ".html", expected: "text/html" }, + { ext: ".css", expected: "text/css" }, + { ext: ".js", expected: "application/javascript" }, + { ext: ".ts", expected: "text/typescript" }, + { ext: ".md", expected: "text/markdown" }, + { ext: ".png", expected: "image/png" }, + { ext: ".jpg", expected: "image/jpeg" }, + { ext: ".pdf", expected: "application/pdf" }, + { ext: ".unknown", expected: "application/octet-stream" }, + ]; + + for (const { ext, expected } of testCases) { + test(`should detect ${expected} for ${ext} files`, async () => { + await storage.write(`file${ext}`, "content"); + const metadata = await storage.getMetadata(`file${ext}`); + expect(metadata.mimeType).toBe(expected); + }); + } + }); + + describe("writeStream", () => { + test("should stream content to file", async () => { + const content = "Hello from stream!"; + const chunks = [Buffer.from(content)]; + + const stream = new Readable({ + read() { + const chunk = chunks.shift(); + this.push(chunk ?? null); + }, + }); + + const result = await storage.writeStream("stream-test.txt", stream); + + expect(result.bytesWritten).toBe(content.length); + expect(result.file.path).toBe("stream-test.txt"); + + const readBack = await storage.read("stream-test.txt"); + expect(readBack.content).toBe(content); + }); + + test("should stream large content without buffering", async () => { + // Create a 1MB stream in chunks + const chunkSize = 64 * 1024; // 64KB chunks + const totalSize = 1024 * 1024; // 1MB + let bytesGenerated = 0; + + const stream = new Readable({ + read() { + if (bytesGenerated >= totalSize) { + this.push(null); + return; + } + const size = Math.min(chunkSize, totalSize - bytesGenerated); + const chunk = Buffer.alloc(size, "x"); + bytesGenerated += size; + this.push(chunk); + }, + }); + + const result = await storage.writeStream("large-stream.bin", stream); + + expect(result.bytesWritten).toBe(totalSize); + + const metadata = await storage.getMetadata("large-stream.bin"); + expect(metadata.size).toBe(totalSize); + }); + + test("should create parent directories", async () => { + const stream = Readable.from([Buffer.from("nested content")]); + + const result = await storage.writeStream( + "deep/nested/path/file.txt", + stream, + { createParents: true }, + ); + + expect(result.file.path).toBe("deep/nested/path/file.txt"); + + const readBack = await storage.read("deep/nested/path/file.txt"); + expect(readBack.content).toBe("nested content"); + }); + + test("should fail if file exists and overwrite is false", async () => { + await storage.write("existing-stream.txt", "existing"); + + const stream = Readable.from([Buffer.from("new content")]); + + await expect( + storage.writeStream("existing-stream.txt", stream, { + overwrite: false, + }), + ).rejects.toThrow("File already exists"); + }); + + test("should overwrite if overwrite is true", async () => { + await storage.write("overwrite-stream.txt", "old"); + + const stream = Readable.from([Buffer.from("new content")]); + await storage.writeStream("overwrite-stream.txt", stream, { + overwrite: true, + }); + + const readBack = await storage.read("overwrite-stream.txt"); + expect(readBack.content).toBe("new content"); + }); + }); +}); + +describe("getExtensionFromMimeType", () => { + test("should return extension for known MIME types", () => { + expect(getExtensionFromMimeType("application/json")).toBe(".json"); + expect(getExtensionFromMimeType("image/png")).toBe(".png"); + expect(getExtensionFromMimeType("text/plain")).toBe(".txt"); + // .htm is shorter than .html so it's preferred + expect(getExtensionFromMimeType("text/html")).toBe(".htm"); + expect(getExtensionFromMimeType("application/pdf")).toBe(".pdf"); + }); + + test("should handle MIME types with charset", () => { + expect(getExtensionFromMimeType("application/json; charset=utf-8")).toBe( + ".json", + ); + expect(getExtensionFromMimeType("text/html; charset=UTF-8")).toBe(".htm"); + }); + + test("should return .ndjson for newline-delimited JSON", () => { + expect(getExtensionFromMimeType("application/x-ndjson")).toBe(".ndjson"); + expect(getExtensionFromMimeType("application/jsonl")).toBe(".jsonl"); + }); + + test("should return empty string for unknown MIME types", () => { + expect(getExtensionFromMimeType("application/x-unknown-format")).toBe(""); + }); + + test("should return .bin for octet-stream", () => { + expect(getExtensionFromMimeType("application/octet-stream")).toBe(".bin"); + }); +}); diff --git a/local-fs/server/storage.ts b/local-fs/server/storage.ts new file mode 100644 index 00000000..1b0b0ef4 --- /dev/null +++ b/local-fs/server/storage.ts @@ -0,0 +1,487 @@ +/** + * Local File Storage Implementation + * + * Portable filesystem operations that work with any mounted path. + */ + +import { + mkdir, + readFile, + writeFile, + unlink, + stat, + readdir, + rename, + copyFile, + rm, + open, +} from "node:fs/promises"; +import { dirname, basename, extname, resolve } from "node:path"; +import { existsSync } from "node:fs"; +import { Readable } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import { logOp } from "./logger.js"; + +/** + * File entity returned by listing/metadata operations + */ +export interface FileEntity { + id: string; + title: string; + path: string; + parent: string; + mimeType: string; + size: number; + isDirectory: boolean; + created_at: string; + updated_at: string; +} + +/** + * MIME type lookup based on file extension + */ +const MIME_TYPES: Record = { + // Text + ".txt": "text/plain", + ".html": "text/html", + ".htm": "text/html", + ".css": "text/css", + ".csv": "text/csv", + // JavaScript/TypeScript + ".js": "application/javascript", + ".mjs": "application/javascript", + ".jsx": "text/javascript", + ".ts": "text/typescript", + ".tsx": "text/typescript", + // Data formats + ".json": "application/json", + ".xml": "application/xml", + ".yaml": "text/yaml", + ".yml": "text/yaml", + ".toml": "text/toml", + // Markdown + ".md": "text/markdown", + ".mdx": "text/mdx", + ".markdown": "text/markdown", + // Images + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".avif": "image/avif", + // Documents + ".pdf": "application/pdf", + ".doc": "application/msword", + ".docx": + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".xls": "application/vnd.ms-excel", + ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + // Archives + ".zip": "application/zip", + ".tar": "application/x-tar", + ".gz": "application/gzip", + // Audio + ".mp3": "audio/mpeg", + ".wav": "audio/wav", + ".ogg": "audio/ogg", + // Video + ".mp4": "video/mp4", + ".webm": "video/webm", + ".mov": "video/quicktime", +}; + +function getMimeType(filename: string): string { + const ext = extname(filename).toLowerCase(); + return MIME_TYPES[ext] || "application/octet-stream"; +} + +/** + * Reverse MIME type lookup - get extension from MIME type + */ +const MIME_TO_EXT: Record = Object.entries(MIME_TYPES).reduce( + (acc, [ext, mime]) => { + // Don't overwrite if already set (prefer shorter extensions) + if (!acc[mime] || ext.length < acc[mime].length) { + acc[mime] = ext; + } + return acc; + }, + {} as Record, +); + +// Add common MIME types that might not have extensions in our map +Object.assign(MIME_TO_EXT, { + "application/octet-stream": ".bin", + "text/plain": ".txt", + "application/x-ndjson": ".ndjson", + "application/jsonl": ".jsonl", + "application/x-jsonlines": ".jsonl", +}); + +export function getExtensionFromMimeType(mimeType: string): string { + // Handle charset suffix (e.g., "application/json; charset=utf-8") + const baseMime = mimeType.split(";")[0].trim().toLowerCase(); + return MIME_TO_EXT[baseMime] || ""; +} + +function sanitizePath(path: string): string { + // Normalize backslashes to forward slashes (Windows compatibility) + return path + .replace(/\\/g, "/") + .split("/") + .filter((segment) => segment !== ".." && segment !== ".") + .join("/") + .replace(/^\/+/, ""); +} + +/** + * Local File Storage class + */ +export class LocalFileStorage { + private rootDir: string; + + constructor(rootDir: string) { + this.rootDir = resolve(rootDir); + } + + get root(): string { + return this.rootDir; + } + + /** + * Normalize a path by stripping the root directory prefix if present. + * This handles cases where AI agents mistakenly include the full root path. + */ + normalizePath(path: string): string { + let normalizedPath = path; + + // Strip root directory prefix if the path starts with it + // Must check for trailing slash, colon, or exact match to avoid matching paths like + // /tmp/rootEvil when root is /tmp/root + const rootWithSlash = this.rootDir + "/"; + const rootWithColon = this.rootDir + ":"; + if (normalizedPath.startsWith(rootWithSlash)) { + normalizedPath = normalizedPath.slice(rootWithSlash.length); + } else if (normalizedPath.startsWith(rootWithColon)) { + // Handle colon separator (e.g., "/path/to/root:filename.png") + normalizedPath = normalizedPath.slice(rootWithColon.length); + } else if (normalizedPath === this.rootDir) { + // Exact match - return root + normalizedPath = ""; + } + + // Handle standalone colon at start (edge case) + if (normalizedPath.startsWith(":")) { + normalizedPath = normalizedPath.slice(1); + } + + // Strip leading slashes + normalizedPath = normalizedPath.replace(/^\/+/, ""); + + return normalizedPath; + } + + /** + * Resolve a relative path to an absolute path within the storage root. + * Public for use by tools that need the absolute path (e.g., GET_PRESIGNED_URL). + */ + resolvePath(path: string): string { + const normalizedPath = this.normalizePath(path); + const sanitized = sanitizePath(normalizedPath); + const resolved = resolve(this.rootDir, sanitized); + + // Defense-in-depth: verify resolved path is within rootDir + if (!resolved.startsWith(this.rootDir)) { + throw new Error("Path traversal attempt detected"); + } + + return resolved; + } + + private async ensureDir(dir: string): Promise { + if (!existsSync(dir)) { + await mkdir(dir, { recursive: true }); + } + } + + async getMetadata(path: string): Promise { + const fullPath = this.resolvePath(path); + const stats = await stat(fullPath); + const name = basename(path) || path; + const parentPath = dirname(path); + const parent = parentPath === "." || parentPath === "/" ? "" : parentPath; + const isDirectory = stats.isDirectory(); + const mimeType = isDirectory ? "inode/directory" : getMimeType(name); + + return { + id: path || "/", + title: parent ? path : name || "Root", + path: path || "/", + parent, + mimeType, + size: stats.size, + isDirectory, + created_at: stats.birthtime.toISOString(), + updated_at: stats.mtime.toISOString(), + }; + } + + async read( + path: string, + encoding: "utf-8" | "base64" = "utf-8", + ): Promise<{ content: string; metadata: FileEntity }> { + const fullPath = this.resolvePath(path); + const buffer = await readFile(fullPath); + const content = + encoding === "base64" + ? buffer.toString("base64") + : buffer.toString("utf-8"); + const metadata = await this.getMetadata(path); + logOp("READ", path, { size: buffer.length }); + return { content, metadata }; + } + + async write( + path: string, + content: string, + options: { + encoding?: "utf-8" | "base64"; + createParents?: boolean; + overwrite?: boolean; + } = {}, + ): Promise<{ file: FileEntity }> { + const fullPath = this.resolvePath(path); + + if (options.createParents !== false) { + await this.ensureDir(dirname(fullPath)); + } + + if (options.overwrite === false && existsSync(fullPath)) { + throw new Error(`File already exists: ${path}`); + } + + const buffer = + options.encoding === "base64" + ? Buffer.from(content, "base64") + : Buffer.from(content, "utf-8"); + + await writeFile(fullPath, buffer); + const file = await this.getMetadata(path); + logOp("WRITE", path, { size: buffer.length }); + return { file }; + } + + async delete( + path: string, + recursive = false, + ): Promise<{ success: boolean; path: string }> { + const fullPath = this.resolvePath(path); + const stats = await stat(fullPath); + + if (stats.isDirectory()) { + if (!recursive) { + throw new Error("Cannot delete directory without recursive flag"); + } + await rm(fullPath, { recursive: true, force: true }); + } else { + await unlink(fullPath); + } + + logOp("DELETE", path); + return { success: true, path }; + } + + async list( + folder = "", + options: { recursive?: boolean; filesOnly?: boolean } = {}, + ): Promise { + const fullPath = this.resolvePath(folder); + + if (!existsSync(fullPath)) { + return []; + } + + if (options.recursive) { + const files = await this.listRecursive(folder, options.filesOnly); + logOp("LIST", folder || "/", { count: files.length, recursive: true }); + return files; + } + + const entries = await readdir(fullPath, { withFileTypes: true }); + let files: FileEntity[] = []; + + for (const entry of entries) { + if (entry.name.startsWith(".")) continue; + + // Skip directories if filesOnly is true + if (options.filesOnly && entry.isDirectory()) continue; + + const entryPath = folder ? `${folder}/${entry.name}` : entry.name; + try { + const metadata = await this.getMetadata(entryPath); + files.push(metadata); + } catch { + continue; + } + } + + // Sort: directories first, then by name (only relevant if not filesOnly) + files = files.sort((a, b) => { + if (a.isDirectory && !b.isDirectory) return -1; + if (!a.isDirectory && b.isDirectory) return 1; + return a.title.localeCompare(b.title); + }); + + logOp("LIST", folder || "/", { count: files.length }); + return files; + } + + private async listRecursive( + folder = "", + filesOnly = false, + ): Promise { + const fullPath = this.resolvePath(folder); + + if (!existsSync(fullPath)) { + return []; + } + + const entries = await readdir(fullPath, { withFileTypes: true }); + const files: FileEntity[] = []; + + for (const entry of entries) { + if (entry.name.startsWith(".")) continue; + + const entryPath = folder ? `${folder}/${entry.name}` : entry.name; + + try { + const metadata = await this.getMetadata(entryPath); + + if (entry.isDirectory()) { + if (!filesOnly) { + files.push(metadata); + } + const subFiles = await this.listRecursive(entryPath, filesOnly); + files.push(...subFiles); + } else { + files.push(metadata); + } + } catch { + continue; + } + } + + return files; + } + + async mkdir(path: string, recursive = true): Promise<{ folder: FileEntity }> { + const fullPath = this.resolvePath(path); + await mkdir(fullPath, { recursive }); + const metadata = await this.getMetadata(path); + logOp("MKDIR", path); + return { folder: metadata }; + } + + async move( + from: string, + to: string, + overwrite = false, + ): Promise<{ file: FileEntity }> { + const fromPath = this.resolvePath(from); + const toPath = this.resolvePath(to); + + if (!overwrite && existsSync(toPath)) { + throw new Error(`Destination already exists: ${to}`); + } + + await this.ensureDir(dirname(toPath)); + await rename(fromPath, toPath); + const file = await this.getMetadata(to); + logOp("MOVE", from, { to }); + return { file }; + } + + async copy( + from: string, + to: string, + overwrite = false, + ): Promise<{ file: FileEntity }> { + const fromPath = this.resolvePath(from); + const toPath = this.resolvePath(to); + + if (!overwrite && existsSync(toPath)) { + throw new Error(`Destination already exists: ${to}`); + } + + await this.ensureDir(dirname(toPath)); + await copyFile(fromPath, toPath); + const file = await this.getMetadata(to); + logOp("COPY", from, { to }); + return { file }; + } + + /** + * Write a readable stream directly to disk without buffering in memory. + * Used for streaming large downloads directly to filesystem. + */ + async writeStream( + path: string, + stream: ReadableStream | NodeJS.ReadableStream, + options: { + createParents?: boolean; + overwrite?: boolean; + } = {}, + ): Promise<{ file: FileEntity; bytesWritten: number }> { + const fullPath = this.resolvePath(path); + + if (options.createParents !== false) { + await this.ensureDir(dirname(fullPath)); + } + + if (options.overwrite === false && existsSync(fullPath)) { + throw new Error(`File already exists: ${path}`); + } + + // Convert Web ReadableStream to Node.js Readable if needed + const nodeStream = + stream instanceof Readable + ? stream + : Readable.fromWeb( + stream as unknown as import("stream/web").ReadableStream, + ); + + // Track bytes written + let bytesWritten = 0; + + // Create write stream + const fileHandle = await open(fullPath, "w"); + const writeStream = fileHandle.createWriteStream(); + + // Create a passthrough that counts bytes + const countingStream = new Readable({ + read() {}, + }); + + nodeStream.on("data", (chunk: Buffer) => { + bytesWritten += chunk.length; + countingStream.push(chunk); + }); + + nodeStream.on("end", () => { + countingStream.push(null); + }); + + nodeStream.on("error", (err) => { + countingStream.destroy(err); + }); + + await pipeline(countingStream, writeStream); + + const file = await this.getMetadata(path); + logOp("WRITE_STREAM", path, { size: bytesWritten }); + return { file, bytesWritten }; + } +} diff --git a/local-fs/server/tools.ts b/local-fs/server/tools.ts new file mode 100644 index 00000000..d150d1f8 --- /dev/null +++ b/local-fs/server/tools.ts @@ -0,0 +1,1426 @@ +/** + * MCP Local FS - Tool Definitions + * + * This module contains all tool definitions following the official + * MCP filesystem server schema, plus collection bindings for Mesh. + * + * Uses registerTool() for proper annotation/hint support. + * + * @see https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod"; +import { + LocalFileStorage, + type FileEntity, + getExtensionFromMimeType, +} from "./storage.js"; +import { logTool } from "./logger.js"; + +/** + * Wrap a tool handler with logging + */ +function withLogging>( + toolName: string, + handler: (args: T) => Promise, +): (args: T) => Promise { + return async (args: T) => { + logTool(toolName, args as Record); + const result = await handler(args); + return result; + }; +} + +/** + * Register all filesystem tools on an MCP server + */ +export function registerTools(server: McpServer, storage: LocalFileStorage) { + // ============================================================ + // OFFICIAL MCP FILESYSTEM TOOLS + // Following exact schema from modelcontextprotocol/servers + // ============================================================ + + // read_file - primary file reading tool (official MCP name) + server.registerTool( + "read_file", + { + title: "Read File", + description: + "Read the complete contents of a file from the file system. " + + "Handles various text encodings and provides detailed error messages " + + "if the file cannot be read. Use this tool when you need to examine " + + "the contents of a single file. Only works within allowed directories.", + inputSchema: { + path: z.string().describe("Path to the file to read"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("read_file", async (args) => + readTextFileHandler(storage, args), + ), + ); + + // read_text_file - alias for read_file with head/tail support + server.registerTool( + "read_text_file", + { + title: "Read Text File", + description: + "Read the complete contents of a file from the file system as text. " + + "Handles various text encodings and provides detailed error messages " + + "if the file cannot be read. Use this tool when you need to examine " + + "the contents of a single file. Use the 'head' parameter to read only " + + "the first N lines of a file, or the 'tail' parameter to read only " + + "the last N lines of a file. Only works within allowed directories.", + inputSchema: { + path: z.string().describe("Path to the file to read"), + tail: z + .number() + .optional() + .describe("If provided, returns only the last N lines of the file"), + head: z + .number() + .optional() + .describe("If provided, returns only the first N lines of the file"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("read_text_file", async (args) => + readTextFileHandler(storage, args), + ), + ); + + // read_media_file - read binary files as base64 + server.registerTool( + "read_media_file", + { + title: "Read Media File", + description: + "Read an image or audio file. Returns the base64 encoded data and MIME type. " + + "Only works within allowed directories.", + inputSchema: { + path: z.string().describe("Path to the media file to read"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("read_media_file", async (args): Promise => { + try { + const result = await storage.read(args.path, "base64"); + const mimeType = result.metadata.mimeType; + const type = mimeType.startsWith("image/") + ? "image" + : mimeType.startsWith("audio/") + ? "audio" + : "blob"; + + const contentItem = { + type: type as "image" | "audio", + data: result.content, + mimeType, + }; + + // NOTE: Do NOT include structuredContent for media files + // The base64 data would get serialized to JSON and cause token explosion + return { + content: [contentItem], + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // read_multiple_files - read multiple files at once + server.registerTool( + "read_multiple_files", + { + title: "Read Multiple Files", + description: + "Read the contents of multiple files simultaneously. This is more " + + "efficient than reading files one by one when you need to analyze " + + "or compare multiple files. Each file's content is returned with its " + + "path as a reference. Failed reads for individual files won't stop " + + "the entire operation. Only works within allowed directories.", + inputSchema: { + paths: z + .array(z.string()) + .min(1) + .describe( + "Array of file paths to read. Each path must be a string pointing to a valid file.", + ), + }, + annotations: { readOnlyHint: true }, + }, + withLogging( + "read_multiple_files", + async (args): Promise => { + const results = await Promise.all( + args.paths.map(async (filePath: string) => { + try { + const result = await storage.read(filePath, "utf-8"); + return `${filePath}:\n${result.content}\n`; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + return `${filePath}: Error - ${errorMessage}`; + } + }), + ); + const text = results.join("\n---\n"); + return { + content: [{ type: "text", text }], + structuredContent: { content: text }, + }; + }, + ), + ); + + // write_file - write content to a file + server.registerTool( + "write_file", + { + title: "Write File", + description: + "Create a new file or completely overwrite an existing file with new content. " + + "Use with caution as it will overwrite existing files without warning. " + + "Handles text content with proper encoding. Only works within allowed directories.", + inputSchema: { + path: z.string().describe("Path where the file should be written"), + content: z.string().describe("Content to write to the file"), + }, + annotations: { + readOnlyHint: false, + idempotentHint: true, + destructiveHint: true, + }, + }, + withLogging("write_file", async (args): Promise => { + try { + await storage.write(args.path, args.content, { + encoding: "utf-8", + createParents: true, + overwrite: true, + }); + const text = `Successfully wrote to ${args.path}`; + return { + content: [{ type: "text", text }], + structuredContent: { content: text }, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // edit_file - make search/replace edits with diff preview + server.registerTool( + "edit_file", + { + title: "Edit File", + description: + "Make line-based edits to a text file. Each edit replaces exact text sequences " + + "with new content. Returns a git-style diff showing the changes made. " + + "Only works within allowed directories.", + inputSchema: { + path: z.string().describe("Path to the file to edit"), + edits: z.array( + z.object({ + oldText: z + .string() + .describe("Text to search for - must match exactly"), + newText: z.string().describe("Text to replace with"), + }), + ), + dryRun: z + .boolean() + .default(false) + .describe("Preview changes using git-style diff format"), + }, + annotations: { + readOnlyHint: false, + idempotentHint: false, + destructiveHint: true, + }, + }, + withLogging("edit_file", async (args): Promise => { + try { + const result = await storage.read(args.path, "utf-8"); + let content = result.content; + const originalContent = content; + + // Apply all edits + for (const edit of args.edits) { + if (!content.includes(edit.oldText)) { + return { + content: [ + { + type: "text", + text: `Error: Could not find text to replace: "${edit.oldText.slice(0, 50)}..."`, + }, + ], + isError: true, + }; + } + content = content.replace(edit.oldText, edit.newText); + } + + // Generate diff + const diff = generateDiff(args.path, originalContent, content); + + if (args.dryRun) { + return { + content: [ + { + type: "text", + text: `Dry run - changes not applied:\n\n${diff}`, + }, + ], + structuredContent: { content: diff, dryRun: true }, + }; + } + + // Apply changes + await storage.write(args.path, content, { + encoding: "utf-8", + createParents: false, + overwrite: true, + }); + + return { + content: [{ type: "text", text: diff }], + structuredContent: { content: diff }, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // create_directory - create directories + server.registerTool( + "create_directory", + { + title: "Create Directory", + description: + "Create a new directory or ensure a directory exists. Can create multiple " + + "nested directories in one operation. If the directory already exists, " + + "this operation will succeed silently. Only works within allowed directories.", + inputSchema: { + path: z.string().describe("Path of the directory to create"), + }, + annotations: { + readOnlyHint: false, + idempotentHint: true, + destructiveHint: false, + }, + }, + withLogging("create_directory", async (args): Promise => { + try { + await storage.mkdir(args.path, true); + const text = `Successfully created directory ${args.path}`; + return { + content: [{ type: "text", text }], + structuredContent: { content: text }, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // list_directory - simple directory listing + server.registerTool( + "list_directory", + { + title: "List Directory", + description: + "Get a detailed listing of all files and directories in a specified path. " + + "Results clearly distinguish between files and directories with [FILE] and [DIR] " + + "prefixes. Only works within allowed directories.", + inputSchema: { + path: z.string().describe("Path of the directory to list"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("list_directory", async (args): Promise => { + try { + const items = await storage.list(args.path); + const formatted = items + .map( + (entry) => + `${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.title}`, + ) + .join("\n"); + return { + content: [{ type: "text", text: formatted || "Empty directory" }], + structuredContent: { content: formatted }, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // list_directory_with_sizes - listing with file sizes + server.registerTool( + "list_directory_with_sizes", + { + title: "List Directory with Sizes", + description: + "Get a detailed listing of all files and directories in a specified path, including sizes. " + + "Results clearly distinguish between files and directories. " + + "Only works within allowed directories.", + inputSchema: { + path: z.string().describe("Path of the directory to list"), + sortBy: z + .enum(["name", "size"]) + .optional() + .default("name") + .describe("Sort entries by name or size"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging( + "list_directory_with_sizes", + async (args): Promise => { + try { + const items = await storage.list(args.path); + + // Sort entries + const sortedItems = [...items].sort((a, b) => { + if (args.sortBy === "size") { + return b.size - a.size; + } + return a.title.localeCompare(b.title); + }); + + // Format output + const formatted = sortedItems + .map( + (entry) => + `${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.title.padEnd(30)} ${ + entry.isDirectory ? "" : formatSize(entry.size).padStart(10) + }`, + ) + .join("\n"); + + // Summary + const totalFiles = items.filter((e) => !e.isDirectory).length; + const totalDirs = items.filter((e) => e.isDirectory).length; + const totalSize = items.reduce( + (sum, entry) => sum + (entry.isDirectory ? 0 : entry.size), + 0, + ); + + const summary = `\nTotal: ${totalFiles} files, ${totalDirs} directories\nCombined size: ${formatSize(totalSize)}`; + const text = formatted + summary; + + return { + content: [{ type: "text", text }], + structuredContent: { content: text }, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }, + ), + ); + + // directory_tree - recursive tree view as JSON + server.registerTool( + "directory_tree", + { + title: "Directory Tree", + description: + "Get a recursive tree view of files and directories as a JSON structure. " + + "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " + + "Only works within allowed directories.", + inputSchema: { + path: z.string().describe("Path of the root directory for the tree"), + excludePatterns: z + .array(z.string()) + .optional() + .default([]) + .describe("Glob patterns to exclude from the tree"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("directory_tree", async (args): Promise => { + try { + const tree = await buildDirectoryTree( + storage, + args.path, + args.excludePatterns, + ); + const text = JSON.stringify(tree, null, 2); + return { + content: [{ type: "text", text }], + structuredContent: { content: text }, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // move_file - move or rename files + server.registerTool( + "move_file", + { + title: "Move File", + description: + "Move or rename files and directories. Can move files between directories " + + "and rename them in a single operation. If the destination exists, the " + + "operation will fail. Only works within allowed directories.", + inputSchema: { + source: z.string().describe("Source path of the file or directory"), + destination: z.string().describe("Destination path"), + }, + annotations: { + readOnlyHint: false, + idempotentHint: false, + destructiveHint: false, + }, + }, + withLogging("move_file", async (args): Promise => { + try { + await storage.move(args.source, args.destination, false); + const text = `Successfully moved ${args.source} to ${args.destination}`; + return { + content: [{ type: "text", text }], + structuredContent: { content: text }, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // search_files - search with glob patterns + server.registerTool( + "search_files", + { + title: "Search Files", + description: + "Recursively search for files and directories matching a pattern. " + + "Searches file names (not content). Returns full paths to all matching items. " + + "Only searches within allowed directories.", + inputSchema: { + path: z.string().describe("Starting directory for the search"), + pattern: z + .string() + .describe("Search pattern (supports * and ** wildcards)"), + excludePatterns: z + .array(z.string()) + .optional() + .default([]) + .describe("Patterns to exclude from search"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("search_files", async (args): Promise => { + try { + const results = await searchFiles( + storage, + args.path, + args.pattern, + args.excludePatterns, + ); + const text = + results.length > 0 ? results.join("\n") : "No matches found"; + return { + content: [{ type: "text", text }], + structuredContent: { content: text, matches: results }, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // get_file_info - get detailed file metadata + server.registerTool( + "get_file_info", + { + title: "Get File Info", + description: + "Retrieve detailed metadata about a file or directory. Returns comprehensive " + + "information including size, creation time, last modified time, and type. " + + "Only works within allowed directories.", + inputSchema: { + path: z.string().describe("Path to the file or directory"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("get_file_info", async (args): Promise => { + try { + const metadata = await storage.getMetadata(args.path); + const info = { + path: metadata.path, + type: metadata.isDirectory ? "directory" : "file", + size: metadata.size, + sizeFormatted: formatSize(metadata.size), + mimeType: metadata.mimeType, + created: metadata.created_at, + modified: metadata.updated_at, + }; + const text = Object.entries(info) + .map(([key, value]) => `${key}: ${value}`) + .join("\n"); + return { + content: [{ type: "text", text }], + structuredContent: info, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // list_allowed_directories - show the root directory + server.registerTool( + "list_allowed_directories", + { + title: "List Allowed Directories", + description: + "Returns the list of directories that this server is allowed to access. " + + "Use this to understand which directories are available.", + inputSchema: {}, + annotations: { readOnlyHint: true }, + }, + withLogging( + "list_allowed_directories", + async (): Promise => { + const text = `Allowed directories:\n${storage.root}`; + return { + content: [{ type: "text", text }], + structuredContent: { directories: [storage.root] }, + }; + }, + ), + ); + + // ============================================================ + // ADDITIONAL TOOLS (not in official, but useful) + // ============================================================ + + // delete_file - delete files or directories (official doesn't have this!) + server.registerTool( + "delete_file", + { + title: "Delete File", + description: + "Delete a file or directory. Use recursive=true to delete non-empty directories. " + + "Use with caution as this operation cannot be undone. Only works within allowed directories.", + inputSchema: { + path: z.string().describe("Path to the file or directory to delete"), + recursive: z + .boolean() + .default(false) + .describe( + "If true, recursively delete directories and their contents", + ), + }, + annotations: { + readOnlyHint: false, + idempotentHint: false, + destructiveHint: true, + }, + }, + withLogging("delete_file", async (args): Promise => { + try { + await storage.delete(args.path, args.recursive); + const text = `Successfully deleted ${args.path}`; + return { + content: [{ type: "text", text }], + structuredContent: { content: text }, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // copy_file - copy files (official doesn't have this!) + server.registerTool( + "copy_file", + { + title: "Copy File", + description: + "Copy a file to a new location. The destination must not exist unless overwrite is true. " + + "Only works within allowed directories.", + inputSchema: { + source: z.string().describe("Source path of the file to copy"), + destination: z.string().describe("Destination path for the copy"), + overwrite: z + .boolean() + .default(false) + .describe("If true, overwrite the destination if it exists"), + }, + annotations: { + readOnlyHint: false, + idempotentHint: true, + destructiveHint: false, + }, + }, + withLogging("copy_file", async (args): Promise => { + try { + await storage.copy(args.source, args.destination, args.overwrite); + const text = `Successfully copied ${args.source} to ${args.destination}`; + return { + content: [{ type: "text", text }], + structuredContent: { content: text }, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // fetch_to_file - fetch URL and stream directly to disk + server.registerTool( + "fetch_to_file", + { + title: "Fetch URL to File", + description: + "Fetch content from a URL and save it directly to disk using streaming. " + + "Content is streamed without loading into memory, making it efficient for large files. " + + "Filename is extracted from URL path or Content-Disposition header. " + + "File extension is intelligently determined from Content-Type when not in filename. " + + "Perfect for downloading large datasets, images, or any remote content without " + + "consuming context window tokens. Only works within allowed directories.", + inputSchema: { + url: z.string().describe("The URL to fetch content from"), + filename: z + .string() + .optional() + .describe( + "Optional filename to save as. If not provided, extracted from URL or Content-Disposition header", + ), + directory: z + .string() + .default("") + .describe( + "Directory to save the file in (relative to storage root). Defaults to root.", + ), + overwrite: z + .boolean() + .default(false) + .describe("If true, overwrite existing file"), + headers: z + .record(z.string(), z.string()) + .optional() + .describe( + "Optional HTTP headers to send with the request (e.g., Authorization)", + ), + }, + annotations: { + readOnlyHint: false, + idempotentHint: false, + destructiveHint: false, + }, + }, + withLogging("fetch_to_file", async (args): Promise => { + try { + const fetchHeaders: Record = { + "User-Agent": "MCP-LocalFS/1.0", + ...(args.headers || {}), + }; + + const response = await fetch(args.url, { + headers: fetchHeaders, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + if (!response.body) { + throw new Error("Response has no body"); + } + + // Determine filename + let filename = args.filename; + + if (!filename) { + // Try Content-Disposition header first + const disposition = response.headers.get("Content-Disposition"); + if (disposition) { + const filenameMatch = disposition.match( + /filename[*]?=(?:UTF-8'')?["']?([^"';\n]+)["']?/i, + ); + if (filenameMatch) { + filename = decodeURIComponent(filenameMatch[1].trim()); + } + } + + // Fall back to URL path + if (!filename) { + const urlObj = new URL(args.url); + const pathParts = urlObj.pathname.split("/").filter(Boolean); + filename = + pathParts.length > 0 + ? pathParts[pathParts.length - 1] + : "download"; + } + } + + // Check if filename has extension, if not try to add from Content-Type + const hasExtension = filename.includes("."); + if (!hasExtension) { + const contentType = response.headers.get("Content-Type"); + if (contentType) { + const ext = getExtensionFromMimeType(contentType); + if (ext) { + filename = filename + ext; + } + } + } + + // Sanitize filename + filename = filename.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_"); + + // Build full path + const directory = args.directory || ""; + const fullPath = directory ? `${directory}/${filename}` : filename; + + // Stream to disk + const result = await storage.writeStream(fullPath, response.body, { + createParents: true, + overwrite: args.overwrite, + }); + + const summary = { + path: result.file.path, + size: result.bytesWritten, + sizeFormatted: formatSize(result.bytesWritten), + mimeType: result.file.mimeType, + url: args.url, + }; + + const text = + `Successfully downloaded ${args.url}\n` + + `Saved to: ${result.file.path}\n` + + `Size: ${formatSize(result.bytesWritten)}`; + + return { + content: [{ type: "text", text }], + structuredContent: summary, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // ============================================================ + // OBJECT STORAGE BINDING TOOLS + // These implement the OBJECT_STORAGE_BINDING for the Files plugin + // ============================================================ + + // LIST_OBJECTS - List files with S3-like interface + server.registerTool( + "LIST_OBJECTS", + { + title: "List Objects", + description: + "List files and folders with S3-compatible interface. " + + "Use prefix to filter by folder path, delimiter '/' to group by folders.", + inputSchema: { + prefix: z + .string() + .optional() + .default("") + .describe( + "Filter objects by prefix (e.g., 'folder/' for folder contents)", + ), + maxKeys: z + .number() + .optional() + .default(1000) + .describe("Maximum number of keys to return"), + continuationToken: z + .string() + .optional() + .describe("Token for pagination (offset as string)"), + delimiter: z + .string() + .optional() + .describe("Delimiter for grouping keys (typically '/')"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("LIST_OBJECTS", async (args): Promise => { + try { + const prefix = args.prefix || ""; + const maxKeys = args.maxKeys || 1000; + const offset = args.continuationToken + ? parseInt(args.continuationToken, 10) + : 0; + const useDelimiter = args.delimiter === "/"; + + // List all items in the prefix directory + const allItems = await storage.list(prefix, { + recursive: !useDelimiter, + filesOnly: false, + }); + + // Build response + const objects: Array<{ + key: string; + size: number; + lastModified: string; + etag: string; + }> = []; + + const commonPrefixes: string[] = []; + const seenPrefixes = new Set(); + + for (const item of allItems) { + if (item.isDirectory) { + if (useDelimiter) { + // Add as common prefix (folder) + const folderPath = item.path.endsWith("/") + ? item.path + : item.path + "/"; + if (!seenPrefixes.has(folderPath)) { + seenPrefixes.add(folderPath); + commonPrefixes.push(folderPath); + } + } + } else { + objects.push({ + key: item.path, + size: item.size, + lastModified: item.updated_at || new Date().toISOString(), + etag: `"${item.id}"`, + }); + } + } + + // Apply pagination + const paginatedObjects = objects.slice(offset, offset + maxKeys); + const hasMore = offset + maxKeys < objects.length; + + const result = { + objects: paginatedObjects, + commonPrefixes: useDelimiter ? commonPrefixes : undefined, + isTruncated: hasMore, + nextContinuationToken: hasMore ? String(offset + maxKeys) : undefined, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // GET_OBJECT_METADATA - Get file metadata + server.registerTool( + "GET_OBJECT_METADATA", + { + title: "Get Object Metadata", + description: "Get metadata for a file (size, type, modified time).", + inputSchema: { + key: z.string().describe("Object key/path to get metadata for"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging( + "GET_OBJECT_METADATA", + async (args): Promise => { + try { + const metadata = await storage.getMetadata(args.key); + + const result = { + contentType: metadata.mimeType, + contentLength: metadata.size, + lastModified: metadata.updated_at || new Date().toISOString(), + etag: `"${metadata.id}"`, + metadata: {}, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }, + ), + ); + + // GET_PRESIGNED_URL - Return file URL for local filesystem + server.registerTool( + "GET_PRESIGNED_URL", + { + title: "Get Presigned URL", + description: + "Get a URL for downloading a file. For local filesystem, returns a file:// URL or base64 data URL for images.", + inputSchema: { + key: z.string().describe("Object key/path to generate URL for"), + expiresIn: z + .number() + .optional() + .describe("Ignored for local filesystem"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("GET_PRESIGNED_URL", async (args): Promise => { + try { + const metadata = await storage.getMetadata(args.key); + const absolutePath = storage.resolvePath(args.key); + + // For images, return data URL for preview in browser + if (metadata.mimeType.startsWith("image/")) { + const fileResult = await storage.read(args.key, "base64"); + const dataUrl = `data:${metadata.mimeType};base64,${fileResult.content}`; + + const result = { + url: dataUrl, + expiresIn: 3600, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + } + + // For other files, return file:// URL + const result = { + url: `file://${absolutePath}`, + expiresIn: 3600, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + } catch (error) { + return { + content: [ + { type: "text", text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } + }), + ); + + // PUT_PRESIGNED_URL - Return upload instructions for local filesystem + server.registerTool( + "PUT_PRESIGNED_URL", + { + title: "Put Presigned URL", + description: + "Get a URL for uploading a file. For local filesystem, returns instructions to use write_file tool.", + inputSchema: { + key: z.string().describe("Object key/path for the upload"), + expiresIn: z + .number() + .optional() + .describe("Ignored for local filesystem"), + contentType: z.string().optional().describe("MIME type (optional)"), + }, + annotations: { readOnlyHint: true }, + }, + withLogging("PUT_PRESIGNED_URL", async (args): Promise => { + // For local filesystem, we can't provide a presigned URL + // Instead, provide the path and instructions + const absolutePath = storage.resolvePath(args.key); + + const result = { + url: `file://${absolutePath}`, + expiresIn: 3600, + // Note: The Files plugin may need to use write_file instead + _note: "Use write_file tool to upload content to this path", + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); + + // DELETE_OBJECT - Delete a single file + server.registerTool( + "DELETE_OBJECT", + { + title: "Delete Object", + description: "Delete a single file or empty directory.", + inputSchema: { + key: z.string().describe("Object key/path to delete"), + }, + annotations: { + readOnlyHint: false, + idempotentHint: false, + destructiveHint: true, + }, + }, + withLogging("DELETE_OBJECT", async (args): Promise => { + try { + await storage.delete(args.key, false); + + const result = { + success: true, + key: args.key, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + } catch (error) { + const result = { + success: false, + key: args.key, + error: (error as Error).message, + }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + } + }), + ); + + // DELETE_OBJECTS - Batch delete files + server.registerTool( + "DELETE_OBJECTS", + { + title: "Delete Objects", + description: "Delete multiple files in batch.", + inputSchema: { + keys: z + .array(z.string()) + .max(1000) + .describe("Array of object keys/paths to delete"), + }, + annotations: { + readOnlyHint: false, + idempotentHint: false, + destructiveHint: true, + }, + }, + withLogging("DELETE_OBJECTS", async (args): Promise => { + const deleted: string[] = []; + const errors: Array<{ key: string; message: string }> = []; + + for (const key of args.keys) { + try { + await storage.delete(key, false); + deleted.push(key); + } catch (error) { + errors.push({ + key, + message: (error as Error).message, + }); + } + } + + const result = { deleted, errors }; + + return { + content: [{ type: "text", text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + }), + ); +} + +// ============================================================ +// HELPER FUNCTIONS +// ============================================================ + +/** + * Handler for read_file and read_text_file + */ +async function readTextFileHandler( + storage: LocalFileStorage, + args: { path: string; head?: number; tail?: number }, +): Promise { + try { + if (args.head && args.tail) { + return { + content: [ + { + type: "text" as const, + text: "Error: Cannot specify both head and tail parameters simultaneously", + }, + ], + isError: true, + }; + } + + const result = await storage.read(args.path, "utf-8"); + let content = result.content; + + if (args.tail) { + const lines = content.split("\n"); + content = lines.slice(-args.tail).join("\n"); + } else if (args.head) { + const lines = content.split("\n"); + content = lines.slice(0, args.head).join("\n"); + } + + return { + content: [{ type: "text" as const, text: content }], + structuredContent: { content }, + }; + } catch (error) { + return { + content: [ + { type: "text" as const, text: `Error: ${(error as Error).message}` }, + ], + isError: true, + }; + } +} + +/** + * Format file size in human-readable format + */ +function formatSize(bytes: number): string { + const units = ["B", "KB", "MB", "GB", "TB"]; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; +} + +/** + * Generate a simple diff between two strings + */ +function generateDiff( + path: string, + original: string, + modified: string, +): string { + const originalLines = original.split("\n"); + const modifiedLines = modified.split("\n"); + + const lines: string[] = [`--- a/${path}`, `+++ b/${path}`]; + + // Simple line-by-line diff + const maxLen = Math.max(originalLines.length, modifiedLines.length); + let inHunk = false; + let hunkStart = 0; + let hunkLines: string[] = []; + + for (let i = 0; i < maxLen; i++) { + const orig = originalLines[i]; + const mod = modifiedLines[i]; + + if (orig !== mod) { + if (!inHunk) { + inHunk = true; + hunkStart = i + 1; + // Add context before + if (i > 0) hunkLines.push(` ${originalLines[i - 1]}`); + } + + if (orig !== undefined) { + hunkLines.push(`-${orig}`); + } + if (mod !== undefined) { + hunkLines.push(`+${mod}`); + } + } else if (inHunk) { + hunkLines.push(` ${orig}`); + // Close hunk after context + lines.push( + `@@ -${hunkStart},${hunkLines.length} +${hunkStart},${hunkLines.length} @@`, + ); + lines.push(...hunkLines); + hunkLines = []; + inHunk = false; + } + } + + if (hunkLines.length > 0) { + lines.push( + `@@ -${hunkStart},${hunkLines.length} +${hunkStart},${hunkLines.length} @@`, + ); + lines.push(...hunkLines); + } + + return lines.join("\n"); +} + +/** + * Build a recursive directory tree + */ +interface TreeEntry { + name: string; + type: "file" | "directory"; + children?: TreeEntry[]; +} + +async function buildDirectoryTree( + storage: LocalFileStorage, + path: string, + excludePatterns: string[], +): Promise { + const items = await storage.list(path); + const result: TreeEntry[] = []; + + for (const item of items) { + // Check exclusions + const shouldExclude = excludePatterns.some((pattern) => { + if (pattern.includes("*")) { + return matchGlob(item.title, pattern); + } + return item.title === pattern; + }); + + if (shouldExclude) continue; + + const entry: TreeEntry = { + name: item.title.split("/").pop() || item.title, + type: item.isDirectory ? "directory" : "file", + }; + + if (item.isDirectory) { + entry.children = await buildDirectoryTree( + storage, + item.path, + excludePatterns, + ); + } + + result.push(entry); + } + + return result; +} + +/** + * Search for files matching a pattern + */ +async function searchFiles( + storage: LocalFileStorage, + basePath: string, + pattern: string, + excludePatterns: string[], +): Promise { + const items = await storage.list(basePath, { recursive: true }); + const results: string[] = []; + + for (const item of items) { + // Check exclusions + const shouldExclude = excludePatterns.some((p) => matchGlob(item.path, p)); + if (shouldExclude) continue; + + // Check pattern match + if (matchGlob(item.path, pattern) || matchGlob(item.title, pattern)) { + results.push(item.path); + } + } + + return results; +} + +/** + * Simple glob pattern matching + */ +function matchGlob(str: string, pattern: string): boolean { + // Convert glob to regex + const regex = pattern + .replace(/\*\*/g, "<<>>") + .replace(/\*/g, "[^/]*") + .replace(/<<>>/g, ".*") + .replace(/\?/g, ".") + .replace(/\./g, "\\."); + + return new RegExp(`^${regex}$`).test(str) || new RegExp(regex).test(str); +} diff --git a/local-fs/tsconfig.json b/local-fs/tsconfig.json new file mode 100644 index 00000000..bfc3e6aa --- /dev/null +++ b/local-fs/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "server", + "declaration": true, + "resolveJsonModule": true + }, + "include": ["server/**/*.ts"], + "exclude": ["node_modules", "dist", "server/**/*.test.ts"] +} diff --git a/mcp-apps-testbed/README.md b/mcp-apps-testbed/README.md new file mode 100644 index 00000000..5f451fc2 --- /dev/null +++ b/mcp-apps-testbed/README.md @@ -0,0 +1,174 @@ +# MCP Apps Testbed + +A reference implementation for testing [MCP Apps (SEP-1865)](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865) — interactive user interfaces for the Model Context Protocol. + +## What is this? + +This is a simple MCP server that provides example tools with associated UI widgets. It's designed to: + +1. **Test MCP Apps integration** in host applications like Mesh +2. **Demonstrate responsive design patterns** for MCP App UIs +3. **Serve as a reference** for building your own MCP Apps + +## Quick Start + +```bash +# Install dependencies +bun install + +# Run the server (uses stdio transport) +bun run dev +``` + +Then add it as a connection in Mesh: + +- **Transport**: STDIO +- **Command**: `bun` +- **Args**: `/path/to/mcp-apps-testbed/server/main.ts` + +## Available Widgets (10 total) + +| Tool | Description | UI Resource | +|------|-------------|-------------| +| `counter` | Interactive counter with +/- controls | `ui://counter-app` | +| `show_metric` | Display a key metric with trend indicator | `ui://metric` | +| `show_progress` | Visual progress bar with percentage | `ui://progress` | +| `greet` | Personalized greeting card | `ui://greeting-app` | +| `show_chart` | Animated bar chart visualization | `ui://chart-app` | +| `start_timer` | Countdown timer with start/pause | `ui://timer` | +| `show_status` | Status badge with colored indicator | `ui://status` | +| `show_quote` | Quote display with attribution | `ui://quote` | +| `show_sparkline` | Compact inline trend chart | `ui://sparkline` | +| `show_code` | Code snippet with syntax styling | `ui://code` | + +## Responsive Design + +All widgets adapt to three display modes: + +| Mode | Height | Layout | Use Case | +|------|--------|--------|----------| +| **Collapsed** | < 450px | Horizontal/compact | Default in chat | +| **Expanded** | 450-750px | Vertical/spacious | Expanded view in chat | +| **View** | > 750px | Full experience | Resource preview | + +### Design Philosophy + +- **Collapsed = Compact**: Content arranged horizontally, essential elements only +- **Expanded = Breathable**: Content stacked vertically, more details visible +- **View = Complete**: All information displayed, full interactivity + +This mirrors how iOS widgets adapt between compact and expanded states. + +### CSS Breakpoints + +```css +/* Default: Collapsed (horizontal) */ +.container { + display: flex; + flex-direction: row; +} + +/* Expanded: Vertical layout */ +@media (min-height: 450px) { + .container { + flex-direction: column; + } +} + +/* View: Full experience */ +@media (min-height: 750px) { + /* Additional details, larger typography */ +} +``` + +## Design Tokens + +The widgets use a consistent design system: + +```javascript +{ + bg: "#ffffff", + bgSubtle: "#f9fafb", + border: "#e5e7eb", + text: "#111827", + textMuted: "#6b7280", + textSubtle: "#9ca3af", + primary: "#6366f1", + success: "#10b981", + destructive: "#ef4444", +} +``` + +## Project Structure + +``` +mcp-apps-testbed/ +├── server/ +│ ├── main.ts # MCP server entry point +│ └── lib/ +│ └── resources.ts # UI widget HTML definitions +├── package.json +└── README.md +``` + +## How MCP Apps Work + +1. **Tools declare UI associations** via `_meta["ui/resourceUri"]` +2. **Resources provide HTML content** with MIME type `text/html;profile=mcp-app` +3. **Host renders in sandboxed iframe** and communicates via JSON-RPC postMessage +4. **UI receives tool input/result** via `ui/initialize` and can call tools back + +### Example Tool Definition + +```typescript +{ + name: "counter", + description: "An interactive counter", + inputSchema: { + type: "object", + properties: { + initialValue: { type: "number", default: 0 } + } + }, + _meta: { "ui/resourceUri": "ui://counter" } +} +``` + +### Example UI Message Handling + +```javascript +window.addEventListener('message', e => { + const msg = JSON.parse(e.data); + + if (msg.method === 'ui/initialize') { + // Access tool input and result + const { toolInput, toolResult, toolName } = msg.params; + + // Initialize your UI... + + // Respond to host + parent.postMessage(JSON.stringify({ + jsonrpc: '2.0', + id: msg.id, + result: {} + }), '*'); + } +}); +``` + +## Development + +```bash +# Run with hot reload +bun run dev + +# Type check +bun run check + +# Build for production +bun run build +``` + +## License + +MIT diff --git a/mcp-apps-testbed/server/lib/resources.ts b/mcp-apps-testbed/server/lib/resources.ts new file mode 100644 index 00000000..f8c49323 --- /dev/null +++ b/mcp-apps-testbed/server/lib/resources.ts @@ -0,0 +1,1185 @@ +/** + * UI Resources for MCP Apps Testbed + * + * Elegant, responsive widgets that adapt to available space: + * + * - Collapsed (< 450px): Horizontal/compact layout + * - Expanded (>= 450px): Vertical/spacious layout + * - View (>= 750px): Full experience with all details + * + * Design follows Mesh's aesthetic: clean, minimal, subtle. + */ + +export const resources = [ + // Core widgets + { + uri: "ui://counter-app", + name: "Counter", + description: "Interactive counter with increment/decrement controls", + mimeType: "text/html;profile=mcp-app", + }, + { + uri: "ui://metric", + name: "Metric Display", + description: "Display a key metric with label and optional trend", + mimeType: "text/html;profile=mcp-app", + }, + { + uri: "ui://progress", + name: "Progress Tracker", + description: "Visual progress bar with percentage and label", + mimeType: "text/html;profile=mcp-app", + }, + { + uri: "ui://greeting-app", + name: "Greeting Card", + description: "Animated personalized greeting", + mimeType: "text/html;profile=mcp-app", + }, + { + uri: "ui://chart-app", + name: "Bar Chart", + description: "Animated bar chart visualization", + mimeType: "text/html;profile=mcp-app", + }, + // New widgets + { + uri: "ui://timer", + name: "Timer", + description: "Countdown timer with start/pause controls", + mimeType: "text/html;profile=mcp-app", + }, + { + uri: "ui://status", + name: "Status Badge", + description: "Status indicator with icon and label", + mimeType: "text/html;profile=mcp-app", + }, + { + uri: "ui://quote", + name: "Quote", + description: "Display a quote or text with attribution", + mimeType: "text/html;profile=mcp-app", + }, + { + uri: "ui://sparkline", + name: "Sparkline", + description: "Compact inline trend chart", + mimeType: "text/html;profile=mcp-app", + }, + { + uri: "ui://code", + name: "Code Snippet", + description: "Syntax-highlighted code display", + mimeType: "text/html;profile=mcp-app", + }, +]; + +// Design tokens matching Mesh's aesthetic +const tokens = { + bg: "#ffffff", + bgSubtle: "#f9fafb", + border: "#e5e7eb", + borderSubtle: "rgba(0,0,0,0.06)", + text: "#111827", + textMuted: "#6b7280", + textSubtle: "#9ca3af", + primary: "#6366f1", // indigo-500 + primaryLight: "#eef2ff", // indigo-50 + success: "#10b981", // emerald-500 + successLight: "#ecfdf5", + destructive: "#ef4444", + destructiveLight: "#fef2f2", +}; + +const apps: Record = { + // ============================================================================ + // Counter Widget + // Collapsed: Horizontal - value on left, controls on right + // Expanded: Vertical - centered with larger controls + // ============================================================================ + "ui://counter-app": ` + + + + + +
+
+ 0 + Counter +
+
+ + +
+
Click buttons to adjust
+
+ + +`, + + // ============================================================================ + // Metric Widget + // Collapsed: Horizontal - metric value prominent, label beside + // Expanded: Vertical - centered with trend indicator + // ============================================================================ + "ui://metric": ` + + + + + +
+
+ Metric + + +
+
+ + 12% +
+
Compared to previous period
+
+ + +`, + + // ============================================================================ + // Progress Widget + // Collapsed: Horizontal bar with percentage + // Expanded: Vertical with label, bar, and details + // ============================================================================ + "ui://progress": ` + + + + + +
+
+ Progress + 0% +
+
+
+
+
0 of 100 completed
+
+ + +`, + + // ============================================================================ + // ADDITIONAL WIDGETS + // ============================================================================ + + // Greeting App + "ui://greeting-app": ` + + + + + +
+
👋
+
+
Hello!
+
Welcome to MCP Apps
+
+
Interactive greeting card
+
+ + +`, + + // Chart App (original) + "ui://chart-app": ` + + + + + +
+

Favorite Fruits Survey

+
+
+
+
+
+ + +`, + + // ============================================================================ + // Timer Widget + // ============================================================================ + "ui://timer": ` + + + + + +
+ Timer + 00:00 +
+ + +
+
+ + +`, + + // ============================================================================ + // Status Badge Widget + // ============================================================================ + "ui://status": ` + + + + + +
+
+
+
All Systems Operational
+
No issues detected
+
+
Just now
+
+ + +`, + + // ============================================================================ + // Quote Widget + // ============================================================================ + "ui://quote": ` + + + + + +
+
"
+
+
The best way to predict the future is to invent it.
+
Alan Kay
+
+
+ + +`, + + // ============================================================================ + // Sparkline Widget + // ============================================================================ + "ui://sparkline": ` + + + + + +
+
+ Requests + 1,234 +
+
+ ↑ 12% +
+ + +`, + + // ============================================================================ + // Code Snippet Widget + // ============================================================================ + "ui://code": ` + + + + + +
+
+ javascript + +
+
function hello() {
+  console.log("Hello, World!");
+}
+
+ + +`, +}; + +export function getResourceHtml(uri: string): string | undefined { + return apps[uri]; +} diff --git a/mcp-apps-testbed/server/main.ts b/mcp-apps-testbed/server/main.ts new file mode 100644 index 00000000..b8996e7a --- /dev/null +++ b/mcp-apps-testbed/server/main.ts @@ -0,0 +1,370 @@ +#!/usr/bin/env bun +/** + * MCP Apps Testbed Server + * + * A simple MCP server for testing MCP Apps (SEP-1865) in Mesh. + * Uses stdio transport - no auth required. + * + * Usage: + * bun server/main.ts + * + * In Mesh, add as STDIO connection: + * Command: bun + * Args: /path/to/mcp-apps-testbed/server/main.ts + */ +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ListResourcesRequestSchema, + ReadResourceRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { resources, getResourceHtml } from "./lib/resources.ts"; + +// Tool definitions with UI associations +const tools = [ + // Core widgets + { + name: "counter", + description: + "An interactive counter widget. Set an initial value and use the UI to adjust it.", + inputSchema: { + type: "object" as const, + properties: { + initialValue: { + type: "number", + default: 0, + description: "Initial counter value", + }, + label: { + type: "string", + default: "Counter", + description: "Label for the counter", + }, + }, + }, + _meta: { "ui/resourceUri": "ui://counter-app" }, + }, + { + name: "show_metric", + description: "Display a key metric with optional trend indicator.", + inputSchema: { + type: "object" as const, + properties: { + label: { type: "string", description: "Metric label" }, + value: { type: "number", description: "The metric value" }, + unit: { + type: "string", + description: "Unit of measurement (e.g., 'ms', 'GB', '$')", + }, + trend: { + type: "number", + description: "Trend percentage (positive = up, negative = down)", + }, + description: { type: "string", description: "Additional context" }, + }, + required: ["label", "value"], + }, + _meta: { "ui/resourceUri": "ui://metric" }, + }, + { + name: "show_progress", + description: "Display a progress bar with label and percentage.", + inputSchema: { + type: "object" as const, + properties: { + label: { + type: "string", + default: "Progress", + description: "Progress label", + }, + value: { type: "number", description: "Current progress value" }, + total: { type: "number", default: 100, description: "Total/max value" }, + }, + required: ["value"], + }, + _meta: { "ui/resourceUri": "ui://progress" }, + }, + // Additional tools + { + name: "greet", + description: + "Generate a personalized greeting displayed in an elegant card.", + inputSchema: { + type: "object" as const, + properties: { + name: { type: "string", description: "Name to greet" }, + message: { type: "string", description: "Optional custom message" }, + }, + required: ["name"], + }, + _meta: { "ui/resourceUri": "ui://greeting-app" }, + }, + { + name: "show_chart", + description: "Display data as an animated bar chart.", + inputSchema: { + type: "object" as const, + properties: { + title: { type: "string", default: "Chart", description: "Chart title" }, + data: { + type: "array", + items: { + type: "object", + properties: { + label: { type: "string" }, + value: { type: "number" }, + }, + required: ["label", "value"], + }, + description: "Data points", + }, + }, + required: ["data"], + }, + _meta: { "ui/resourceUri": "ui://chart-app" }, + }, + // New widgets + { + name: "start_timer", + description: "Display an interactive timer with start/pause controls.", + inputSchema: { + type: "object" as const, + properties: { + seconds: { type: "number", default: 0, description: "Initial seconds" }, + label: { type: "string", default: "Timer", description: "Timer label" }, + }, + }, + _meta: { "ui/resourceUri": "ui://timer" }, + }, + { + name: "show_status", + description: "Display a status badge with icon indicator.", + inputSchema: { + type: "object" as const, + properties: { + status: { type: "string", description: "Status text" }, + description: { type: "string", description: "Additional details" }, + type: { + type: "string", + enum: ["success", "warning", "error", "info"], + default: "success", + }, + timestamp: { type: "string", description: "Timestamp text" }, + }, + required: ["status"], + }, + _meta: { "ui/resourceUri": "ui://status" }, + }, + { + name: "show_quote", + description: "Display a quote with attribution.", + inputSchema: { + type: "object" as const, + properties: { + text: { type: "string", description: "The quote text" }, + author: { type: "string", description: "Quote attribution" }, + }, + required: ["text"], + }, + _meta: { "ui/resourceUri": "ui://quote" }, + }, + { + name: "show_sparkline", + description: "Display a compact trend chart with current value.", + inputSchema: { + type: "object" as const, + properties: { + label: { type: "string", description: "Metric label" }, + value: { type: "string", description: "Current value to display" }, + data: { + type: "array", + items: { type: "number" }, + description: "Array of values for the chart", + }, + trend: { type: "number", description: "Trend percentage" }, + }, + required: ["value", "data"], + }, + _meta: { "ui/resourceUri": "ui://sparkline" }, + }, + { + name: "show_code", + description: "Display a code snippet with syntax highlighting.", + inputSchema: { + type: "object" as const, + properties: { + code: { type: "string", description: "The code to display" }, + language: { + type: "string", + default: "javascript", + description: "Programming language", + }, + }, + required: ["code"], + }, + _meta: { "ui/resourceUri": "ui://code" }, + }, +]; + +// Tool handlers +const toolHandlers: Record< + string, + (args: Record) => { + content: Array<{ type: string; text: string }>; + _meta?: Record; + } +> = { + // Core widgets + counter: (args) => ({ + content: [ + { + type: "text", + text: `Counter "${args.label || "Counter"}" initialized at ${args.initialValue ?? 0}`, + }, + ], + _meta: { "ui/resourceUri": "ui://counter-app" }, + }), + + show_metric: (args) => ({ + content: [ + { + type: "text", + text: JSON.stringify({ + label: args.label, + value: args.value, + unit: args.unit, + trend: args.trend, + }), + }, + ], + _meta: { "ui/resourceUri": "ui://metric" }, + }), + + show_progress: (args) => ({ + content: [ + { + type: "text", + text: `Progress: ${args.value}/${args.total || 100} (${Math.round(((args.value as number) / ((args.total as number) || 100)) * 100)}%)`, + }, + ], + _meta: { "ui/resourceUri": "ui://progress" }, + }), + + // Additional widgets + greet: (args) => ({ + content: [ + { + type: "text", + text: args.message + ? `Hello ${args.name}! ${args.message}` + : `Hello ${args.name}!`, + }, + ], + _meta: { "ui/resourceUri": "ui://greeting-app" }, + }), + + show_chart: (args) => ({ + content: [ + { + type: "text", + text: `Chart "${args.title ?? "Chart"}" with ${(args.data as Array)?.length ?? 0} data points`, + }, + ], + _meta: { "ui/resourceUri": "ui://chart-app" }, + }), + + // New widget handlers + start_timer: (args) => ({ + content: [ + { type: "text", text: `Timer started at ${args.seconds ?? 0} seconds` }, + ], + _meta: { "ui/resourceUri": "ui://timer" }, + }), + + show_status: (args) => ({ + content: [ + { + type: "text", + text: `Status: ${args.status} (${args.type ?? "success"})`, + }, + ], + _meta: { "ui/resourceUri": "ui://status" }, + }), + + show_quote: (args) => ({ + content: [ + { type: "text", text: `"${args.text}" — ${args.author ?? "Unknown"}` }, + ], + _meta: { "ui/resourceUri": "ui://quote" }, + }), + + show_sparkline: (args) => ({ + content: [ + { type: "text", text: `${args.label ?? "Value"}: ${args.value}` }, + ], + _meta: { "ui/resourceUri": "ui://sparkline" }, + }), + + show_code: (args) => ({ + content: [ + { + type: "text", + text: `\`\`\`${args.language ?? "javascript"}\n${args.code}\n\`\`\``, + }, + ], + _meta: { "ui/resourceUri": "ui://code" }, + }), +}; + +async function main() { + const server = new Server( + { name: "mcp-apps-testbed", version: "1.0.0" }, + { capabilities: { tools: {}, resources: {} } }, + ); + + // Handle tools/list + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools, + })); + + // Handle tools/call + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + const handler = toolHandlers[name]; + if (!handler) { + throw new Error(`Unknown tool: ${name}`); + } + return handler(args ?? {}); + }); + + // Handle resources/list + server.setRequestHandler(ListResourcesRequestSchema, async () => ({ + resources, + })); + + // Handle resources/read + server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const { uri } = request.params; + const html = getResourceHtml(uri); + if (!html) { + throw new Error(`Resource not found: ${uri}`); + } + return { + contents: [{ uri, mimeType: "text/html;profile=mcp-app", text: html }], + }; + }); + + // Connect to stdio transport + const transport = new StdioServerTransport(); + await server.connect(transport); + + console.error("[mcp-apps-testbed] MCP server running via stdio"); + console.error("[mcp-apps-testbed] 10 tools with interactive UI widgets"); +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/package.json b/package.json index d71bc53d..2b932dd7 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,16 @@ "check": "bun run --filter=* check", "clean": "rm -rf node_modules */node_modules */dist */.vite */.wrangler */.deco", "prepare": "sh scripts/setup-hooks.sh", - "build": "bun run ./scripts/build-mcp.ts" + "build": "bun run ./scripts/build-mcp.ts", + "gateway": "bun run shared/gateway/server.ts", + "gateway:setup": "bun run shared/gateway/setup.ts" }, "workspaces": [ "apify", + "blog", "blog-post-generator", + "bookmarks", + "brand", "content-scraper", "data-for-seo", "datajud", @@ -36,12 +41,12 @@ "google-forms", "google-gmail", "google-meet", - "google-apps-script", "google-sheets", "google-slides", "google-tag-manager", "grain", "hyperdx", + "local-fs", "mcp-studio", "meta-ads", "nanobanana", @@ -55,14 +60,13 @@ "replicate", "shared", "slack-mcp", + "slides", "sora", "template-minimal", "template-with-view", "tiktok-ads", "veo", - "vtex-docs", "whatsapp", - "whatsapp-management", "whisper" ], "dependencies": { diff --git a/shared/gateway/.gateway.env b/shared/gateway/.gateway.env new file mode 100644 index 00000000..8f6930a1 --- /dev/null +++ b/shared/gateway/.gateway.env @@ -0,0 +1,6 @@ +# MCP Gateway Configuration +# Generated by: bun run gateway:setup + +MCPS=local-fs,blog +GATEWAY_PORT=8000 +LOCAL_FS_PATH="/Users/guilherme/Projects/vibegui.com" diff --git a/shared/gateway/README.md b/shared/gateway/README.md new file mode 100644 index 00000000..891d5cd3 --- /dev/null +++ b/shared/gateway/README.md @@ -0,0 +1,117 @@ +# MCP Gateway + +Serve multiple MCPs through a single `deco link` tunnel with an interactive TUI setup. + +## Why? + +`deco link` creates one tunnel per machine. If you run multiple `serve` commands, they'll share the same tunnel URL and conflict. The gateway solves this by proxying multiple MCPs through different paths on a single port. + +## Quick Start + +```bash +cd mcps + +# Interactive setup (first time) +bun run gateway:setup + +# Start the gateway +bun run gateway +``` + +## Interactive Setup + +The setup wizard lets you: +- Select which MCPs to run +- Configure the local-fs path +- Save your preferences for future runs + +``` +╔══════════════════════════════════════════════════════════════════════╗ +║ MCP Gateway Setup ║ +╚══════════════════════════════════════════════════════════════════════╝ + +Available MCPs: + + [✓] 1. Local FS + Mount a local folder for file operations + + [✓] 2. Blog + AI-powered blog writing with tone of voice guides + + [ ] 3. Bookmarks + Bookmark management with AI enrichment + +Commands: + 1-5 Toggle MCP + a Select all + n Select none + Enter Continue +``` + +Your selection is saved to `.gateway.env` and loaded automatically on subsequent runs. + +## CLI Usage + +You can also skip setup and use CLI flags: + +```bash +# Start with saved config +bun run gateway + +# Override: specific MCPs +bun run gateway --blog --bookmarks + +# Override: local-fs with path +bun run gateway --local-fs --path /Users/me/my-project +``` + +## Available MCPs + +| ID | Name | Port | Description | +|----|------|------|-------------| +| `local-fs` | Local FS | 8001 | Mount a local folder | +| `blog` | Blog | 8002 | Blog writing with tone of voice | +| `bookmarks` | Bookmarks | 8003 | Bookmark management | +| `slides` | Slides | 8004 | Create presentations | +| `brand` | Brand | 8005 | Brand asset management | + +## Routes + +Each MCP is exposed at `/mcp-{id}`: +- `/mcp-local-fs` → local-fs MCP +- `/mcp-blog` → blog MCP +- `/mcp-bookmarks` → bookmarks MCP +- etc. + +## Output + +When the tunnel is ready, you'll see URLs like: + +``` +╔═══════════════════════════════════════════════════════════════════════╗ +║ ✅ Gateway Ready! ║ +╠═══════════════════════════════════════════════════════════════════════╣ +║ ║ +║ Add these MCP URLs to your Deco Mesh: ║ +║ ║ +║ local-fs https://localhost-xxx.deco.host/mcp-local-fs ║ +║ blog https://localhost-xxx.deco.host/mcp-blog ║ +║ bookmarks https://localhost-xxx.deco.host/mcp-bookmarks ║ +║ ║ +╚═══════════════════════════════════════════════════════════════════════╝ +``` + +Add each URL as a Custom MCP in Deco Mesh. + +## Configuration File + +The setup saves to `.gateway.env`: + +```env +# MCP Gateway Configuration +MCPS=local-fs,blog,bookmarks +GATEWAY_PORT=8000 +LOCAL_FS_PATH="/Users/me/my-project" +``` + +Edit this file directly or run `bun run gateway:setup` again. diff --git a/shared/gateway/package.json b/shared/gateway/package.json new file mode 100644 index 00000000..ef99c400 --- /dev/null +++ b/shared/gateway/package.json @@ -0,0 +1,15 @@ +{ + "name": "@decocms/mcp-gateway", + "version": "1.0.0", + "description": "Gateway to serve multiple MCPs through a single tunnel", + "private": true, + "type": "module", + "scripts": { + "start": "bun run server.ts", + "dev": "bun run --hot server.ts", + "setup": "bun run setup.ts" + }, + "devDependencies": { + "typescript": "^5.7.2" + } +} diff --git a/shared/gateway/server.ts b/shared/gateway/server.ts new file mode 100644 index 00000000..c27242dd --- /dev/null +++ b/shared/gateway/server.ts @@ -0,0 +1,416 @@ +#!/usr/bin/env bun +/** + * MCP Gateway - Serve multiple MCPs through a single tunnel + * + * Routes are dynamically generated based on config: + * /mcp-fs → local-fs MCP + * /mcp-blog → blog MCP + * /mcp-bookmarks → bookmarks MCP + * etc. + * + * Usage: + * bun run gateway # Start with saved config + * bun run gateway:setup # Interactive setup + * bun run gateway --fs --blog # Override: specific MCPs + * bun run gateway --path /my/folder # Override: set local-fs path + */ + +import { spawn, type ChildProcess } from "node:child_process"; +import { platform } from "node:os"; +import { resolve, dirname } from "node:path"; +import { existsSync } from "node:fs"; +import { loadConfig, AVAILABLE_MCPS, CONFIG_FILE } from "./setup.ts"; + +interface MCPConfig { + name: string; + path: string; + port: number; + route: string; + enabled: boolean; + process?: ChildProcess; + args?: string[]; +} + +/** + * Parse CLI arguments and merge with saved config + * Returns null if no config exists (triggers setup wizard) + */ +function parseArgs(): { + mcps: MCPConfig[]; + fsPath: string | null; + gatewayPort: number; +} | null { + const args = process.argv.slice(2); + let fsPath: string | null = null; + let explicitMcps = false; + const enabledMcps: Record = {}; + let gatewayPort = 8000; + + // Load saved config first + const savedConfig = loadConfig(); + if (savedConfig) { + gatewayPort = savedConfig.gatewayPort || 8000; + fsPath = savedConfig.localFsPath || null; + for (const mcpId of savedConfig.mcps) { + enabledMcps[mcpId] = true; + } + } + + // Parse CLI args (override saved config) + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === "--path" || arg === "-p") { + fsPath = args[++i]; + continue; + } + + if (arg === "--port") { + gatewayPort = parseInt(args[++i], 10) || 8000; + continue; + } + + // Check for --mcpname flags + if (arg.startsWith("--")) { + const mcpId = arg.slice(2); + const mcp = AVAILABLE_MCPS.find((m) => m.id === mcpId); + if (mcp) { + enabledMcps[mcpId] = true; + explicitMcps = true; + continue; + } + } + + // Positional arg could be fs path + if (!arg.startsWith("-") && !fsPath) { + fsPath = arg; + } + } + + // If CLI specified explicit MCPs, only use those (override saved) + if (explicitMcps) { + for (const mcp of AVAILABLE_MCPS) { + if (!Object.prototype.hasOwnProperty.call(enabledMcps, mcp.id)) { + enabledMcps[mcp.id] = false; + } + } + } + + // If no config and no CLI args, return null to trigger setup + if (!savedConfig && !explicitMcps) { + return null; + } + + // Find MCP directories + const mcpsRoot = resolve(dirname(import.meta.dirname || process.cwd()), ".."); + + // Build MCP configs from AVAILABLE_MCPS + const mcps: MCPConfig[] = AVAILABLE_MCPS.map((mcp) => ({ + name: mcp.id, + path: resolve(mcpsRoot, mcp.path), + port: mcp.port, + route: `/mcp-${mcp.id}`, + enabled: enabledMcps[mcp.id] || false, + args: mcp.id === "local-fs" && fsPath ? ["--path", fsPath] : [], + })); + + return { mcps: mcps.filter((m) => m.enabled), fsPath, gatewayPort }; +} + +/** + * Copy text to clipboard + */ +function copyToClipboard(text: string): Promise { + return new Promise((resolvePromise) => { + const os = platform(); + let cmd: string; + let cmdArgs: string[]; + + if (os === "darwin") { + cmd = "pbcopy"; + cmdArgs = []; + } else if (os === "win32") { + cmd = "clip"; + cmdArgs = []; + } else { + cmd = "xclip"; + cmdArgs = ["-selection", "clipboard"]; + } + + try { + const proc = spawn(cmd, cmdArgs, { stdio: ["pipe", "ignore", "ignore"] }); + proc.stdin?.write(text); + proc.stdin?.end(); + proc.on("close", (code) => resolvePromise(code === 0)); + proc.on("error", () => resolvePromise(false)); + } catch { + resolvePromise(false); + } + }); +} + +// Parse args or run setup wizard if no config +let config = parseArgs(); + +if (!config) { + // No config found, run the setup wizard + const { runSetup } = await import("./setup.ts"); + const savedConfig = await runSetup(); + + if (!savedConfig || savedConfig.mcps.length === 0) { + console.log("\n\x1b[90mNo MCPs selected. Exiting.\x1b[0m\n"); + process.exit(0); + } + + // Re-parse after setup + config = parseArgs(); + if (!config) { + console.error("\n\x1b[31mSetup failed. Please try again.\x1b[0m\n"); + process.exit(1); + } +} + +const { mcps, fsPath, gatewayPort } = config; + +// Validate fs path if provided +if (fsPath && !existsSync(fsPath)) { + console.error(`\n❌ Path does not exist: ${fsPath}\n`); + process.exit(1); +} + +console.log(` +╔═══════════════════════════════════════════════════════════════════════╗ +║ MCP Gateway - Multi-Serve ║ +╠═══════════════════════════════════════════════════════════════════════╣`); + +for (const mcp of mcps) { + const extra = mcp.name === "local-fs" && fsPath ? ` (${fsPath})` : ""; + console.log( + `║ 🔌 ${mcp.name.padEnd(12)} → :${mcp.port} → ${mcp.route.padEnd(20)}${extra.slice(0, 15).padEnd(15)}║`, + ); +} + +console.log(`╚═══════════════════════════════════════════════════════════════════════╝ + +Starting MCP servers... +`); + +// Start each MCP server +for (const mcp of mcps) { + if (!existsSync(mcp.path)) { + console.log(`⚠️ Skipping ${mcp.name}: ${mcp.path} not found`); + mcp.enabled = false; + continue; + } + + const args = ["run", "--hot", mcp.path, ...(mcp.args || [])]; + + mcp.process = spawn("bun", args, { + stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, PORT: mcp.port.toString() }, + }); + + mcp.process.stdout?.on("data", (data) => { + const output = data.toString().trim(); + if (output && !output.includes("running at")) { + console.log(`[${mcp.name}] ${output}`); + } + }); + + mcp.process.stderr?.on("data", (data) => { + const output = data.toString().trim(); + if (output) { + console.error(`[${mcp.name}] ${output}`); + } + }); + + // Handle MCP process crashes + mcp.process.on("exit", (code) => { + if (code !== 0 && code !== null) { + console.error(`[gateway] ⚠️ ${mcp.name} exited with code ${code}`); + } + }); + + mcp.process.on("error", (err) => { + console.error(`[gateway] ⚠️ ${mcp.name} error:`, err.message); + }); + + console.log(`✅ Started ${mcp.name} on port ${mcp.port}`); +} + +// Wait for servers to start +await new Promise((r) => setTimeout(r, 2000)); + +// Create the gateway server that proxies to the appropriate MCP +const enabledMcps = mcps.filter((m) => m.enabled && m.process); + +const server = Bun.serve({ + port: gatewayPort, + idleTimeout: 255, // Max timeout for long MCP requests + async fetch(req) { + const url = new URL(req.url); + + // Find matching MCP + for (const mcp of enabledMcps) { + if (url.pathname.startsWith(mcp.route)) { + let targetUrl: string; + + if (mcp.name === "local-fs" && mcp.args?.length) { + // 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 + const newPath = url.pathname.replace(mcp.route, "/mcp"); + targetUrl = `http://localhost:${mcp.port}${newPath}${url.search}`; + } + + try { + // Clone headers and remove hop-by-hop headers + const proxyHeaders = new Headers(req.headers); + proxyHeaders.delete("host"); + proxyHeaders.delete("connection"); + + const proxyReq = new Request(targetUrl, { + method: req.method, + headers: proxyHeaders, + body: req.body, + // @ts-ignore - Bun supports this + duplex: "half", + }); + + const response = await fetch(proxyReq); + + // Clone response headers + const responseHeaders = new Headers(response.headers); + // Remove transfer-encoding as Bun handles this + responseHeaders.delete("transfer-encoding"); + + return new Response(response.body, { + status: response.status, + headers: responseHeaders, + }); + } catch (error) { + console.error(`[gateway] Proxy error for ${mcp.name}:`, error); + return new Response( + JSON.stringify({ + error: `Failed to proxy to ${mcp.name}: ${error}`, + }), + { status: 502, headers: { "Content-Type": "application/json" } }, + ); + } + } + } + + // Index page with available MCPs + if (url.pathname === "/" || url.pathname === "") { + const mcpList = enabledMcps + .map((m) => ` - ${m.route} → ${m.name}`) + .join("\n"); + + return new Response(`MCP Gateway\n\nAvailable MCPs:\n${mcpList}\n`, { + headers: { "Content-Type": "text/plain" }, + }); + } + + return new Response("Not Found", { status: 404 }); + }, +}); + +console.log(`\n🌐 Gateway running at http://localhost:${gatewayPort}`); +console.log(`\nLocal MCP URLs:`); +for (const mcp of enabledMcps) { + console.log(` - http://localhost:${gatewayPort}${mcp.route}`); +} + +// Start deco link for the gateway +console.log(`\nStarting tunnel...`); + +let publicUrl = ""; + +async function showUrls(tunnelUrl: string) { + if (publicUrl) return; + publicUrl = tunnelUrl; + + const urls = enabledMcps.map((m) => `${publicUrl}${m.route}`); + + console.log(` + +╔═══════════════════════════════════════════════════════════════════════╗ +║ ✅ Gateway Ready! ║ +╠═══════════════════════════════════════════════════════════════════════╣ +║ ║ +║ Add these MCP URLs to your Deco Mesh: ║ +║ ║`); + + for (const mcp of enabledMcps) { + const mcpUrl = `${publicUrl}${mcp.route}`; + console.log(`║ ${mcp.name.padEnd(12)} ${mcpUrl.padEnd(54)}║`); + } + + console.log(`║ ║ +║ Steps: ║ +║ 1. Open mesh-admin.decocms.com ║ +║ 2. Go to Connections → Add Custom MCP ║ +║ 3. Paste any URL above ║ +║ ║ +╚═══════════════════════════════════════════════════════════════════════╝ +`); + + // Copy first URL + const copied = await copyToClipboard(urls[0]); + if (copied) { + console.log(`📋 Copied ${enabledMcps[0].name} URL to clipboard!`); + } +} + +function checkForTunnelUrl(output: string) { + const urlMatch = output.match(/https:\/\/[^\s()"']+\.deco\.(site|host)/); + if (urlMatch) { + const url = urlMatch[0].replace(/[()]/g, ""); + showUrls(url); + } +} + +const decoLink = spawn("deco", ["link", "-p", gatewayPort.toString()], { + stdio: ["inherit", "pipe", "pipe"], +}); + +decoLink.stdout?.on("data", (data) => { + const output = data.toString(); + process.stdout.write(output); + checkForTunnelUrl(output); +}); + +decoLink.stderr?.on("data", (data) => { + const output = data.toString(); + process.stderr.write(output); + checkForTunnelUrl(output); +}); + +// Cleanup on exit +function cleanup() { + for (const mcp of mcps) { + mcp.process?.kill(); + } + decoLink.kill(); + server.stop(); + process.exit(0); +} + +process.on("SIGINT", cleanup); +process.on("SIGTERM", cleanup); + +// Prevent crashes from unhandled errors +process.on("uncaughtException", (error) => { + console.error("[gateway] Uncaught exception:", error.message); +}); + +process.on("unhandledRejection", (reason) => { + console.error("[gateway] Unhandled rejection:", reason); +}); + +decoLink.on("close", (code) => { + cleanup(); +}); diff --git a/shared/gateway/setup.ts b/shared/gateway/setup.ts new file mode 100644 index 00000000..60c6d9d6 --- /dev/null +++ b/shared/gateway/setup.ts @@ -0,0 +1,396 @@ +#!/usr/bin/env bun +/** + * MCP Gateway - Interactive Setup + * + * A fancy TUI to configure which MCPs to run through the gateway. + * Saves configuration to .env for subsequent runs. + */ + +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import * as readline from "node:readline"; + +// ANSI colors and styles (green, gray, purple, golden orange) +const colors = { + reset: "\x1b[0m", + bold: "\x1b[1m", + dim: "\x1b[2m", + gray: "\x1b[90m", + green: "\x1b[32m", + brightGreen: "\x1b[92m", + orange: "\x1b[38;5;214m", // Golden orange + purple: "\x1b[38;5;141m", // Light purple + magenta: "\x1b[35m", + white: "\x1b[37m", + bgGreen: "\x1b[42m", + bgPurple: "\x1b[48;5;141m", +}; + +const c = colors; + +interface MCPOption { + id: string; + name: string; + description: string; + port: number; + path: string; + requiresPath?: boolean; + pathDescription?: string; +} + +const AVAILABLE_MCPS: MCPOption[] = [ + { + id: "local-fs", + name: "Local FS", + description: "Mount a local folder for file operations", + port: 8001, + path: "local-fs/server/http.ts", + requiresPath: true, + pathDescription: "Path to mount (e.g., /Users/you/my-project)", + }, + { + id: "blog", + name: "Blog", + description: "AI-powered blog writing with tone of voice guides", + port: 8002, + path: "blog/server/main.ts", + }, + { + id: "bookmarks", + name: "Bookmarks", + description: "Bookmark management with AI enrichment", + port: 8003, + path: "bookmarks/server/main.ts", + }, + { + id: "slides", + name: "Slides", + description: "Create beautiful presentations", + port: 8004, + path: "slides/server/main.ts", + }, + { + id: "brand", + name: "Brand", + description: "Brand asset and style management", + port: 8005, + path: "brand/server/main.ts", + }, +]; + +interface GatewayConfig { + mcps: string[]; + localFsPath?: string; + gatewayPort: number; +} + +const CONFIG_FILE = resolve( + import.meta.dirname || process.cwd(), + ".gateway.env", +); + +/** + * Load existing config from .env file + */ +function loadConfig(): GatewayConfig | null { + if (!existsSync(CONFIG_FILE)) { + return null; + } + + try { + const content = readFileSync(CONFIG_FILE, "utf-8"); + const lines = content.split("\n"); + const config: GatewayConfig = { + mcps: [], + gatewayPort: 8000, + }; + + for (const line of lines) { + const [key, ...valueParts] = line.split("="); + const value = valueParts.join("=").trim(); + + if (key === "MCPS") { + config.mcps = value.split(",").filter(Boolean); + } else if (key === "LOCAL_FS_PATH") { + config.localFsPath = value.replace(/^["']|["']$/g, ""); + } else if (key === "GATEWAY_PORT") { + config.gatewayPort = parseInt(value, 10) || 8000; + } + } + + return config.mcps.length > 0 ? config : null; + } catch { + return null; + } +} + +/** + * Save config to .env file + */ +function saveConfig(config: GatewayConfig): void { + const lines = [ + `# MCP Gateway Configuration`, + `# Generated by: bun run gateway:setup`, + ``, + `MCPS=${config.mcps.join(",")}`, + `GATEWAY_PORT=${config.gatewayPort}`, + ]; + + if (config.localFsPath) { + lines.push(`LOCAL_FS_PATH="${config.localFsPath}"`); + } + + writeFileSync(CONFIG_FILE, lines.join("\n") + "\n"); +} + +/** + * Create readline interface + */ +function createRL(): readline.Interface { + return readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); +} + +/** + * Ask a question and return the answer + */ +function ask(rl: readline.Interface, question: string): Promise { + return new Promise((resolve) => { + rl.question(question, (answer) => { + resolve(answer.trim()); + }); + }); +} + +/** + * Clear screen and show header + */ +function showHeader(): void { + console.clear(); + console.log(` +${c.purple}${c.bold} ╔══════════════════════════════════════════════════════════╗${c.reset} +${c.purple}${c.bold} ║${c.reset} ${c.brightGreen}${c.bold}MCP Gateway Setup${c.reset} ${c.purple}${c.bold}║${c.reset} +${c.purple}${c.bold} ╚══════════════════════════════════════════════════════════╝${c.reset} + +${c.gray} Configure which MCPs to run through a single tunnel.${c.reset} +${c.gray} Your selection will be saved for future runs.${c.reset} +`); +} + +/** + * Show MCP selection menu + */ +function showMCPMenu(selectedMcps: Set): void { + console.log(` ${c.purple}${c.bold}Available MCPs:${c.reset}\n`); + + for (let i = 0; i < AVAILABLE_MCPS.length; i++) { + const mcp = AVAILABLE_MCPS[i]; + const isSelected = selectedMcps.has(mcp.id); + const checkbox = isSelected + ? `${c.brightGreen}[✓]${c.reset}` + : `${c.gray}[ ]${c.reset}`; + const num = `${c.orange}${i + 1}${c.reset}`; + const name = isSelected + ? `${c.brightGreen}${c.bold}${mcp.name}${c.reset}` + : `${c.white}${mcp.name}${c.reset}`; + + console.log(` ${checkbox} ${num}. ${name}`); + console.log(` ${c.gray}${mcp.description}${c.reset}`); + console.log(); + } +} + +/** + * Interactive MCP selection + */ +async function selectMCPs( + rl: readline.Interface, + existingConfig: GatewayConfig | null, +): Promise> { + const selectedMcps = new Set(existingConfig?.mcps || []); + + while (true) { + showHeader(); + showMCPMenu(selectedMcps); + + console.log(` ${c.gray}Commands:${c.reset}`); + console.log( + ` ${c.orange}1-${AVAILABLE_MCPS.length}${c.reset} ${c.gray}Toggle MCP${c.reset}`, + ); + console.log(` ${c.orange}a${c.reset} ${c.gray}Select all${c.reset}`); + console.log(` ${c.orange}n${c.reset} ${c.gray}Select none${c.reset}`); + console.log(` ${c.orange}↵${c.reset} ${c.gray}Continue${c.reset}\n`); + + const input = await ask(rl, ` ${c.purple}›${c.reset} `); + + if (input === "") { + if (selectedMcps.size === 0) { + console.log(`\n${c.orange}Please select at least one MCP.${c.reset}`); + await ask(rl, `${c.gray}Press Enter to continue...${c.reset}`); + continue; + } + break; + } + + if (input.toLowerCase() === "a") { + for (const mcp of AVAILABLE_MCPS) { + selectedMcps.add(mcp.id); + } + continue; + } + + if (input.toLowerCase() === "n") { + selectedMcps.clear(); + continue; + } + + const num = parseInt(input, 10); + if (num >= 1 && num <= AVAILABLE_MCPS.length) { + const mcp = AVAILABLE_MCPS[num - 1]; + if (selectedMcps.has(mcp.id)) { + selectedMcps.delete(mcp.id); + } else { + selectedMcps.add(mcp.id); + } + } + } + + return selectedMcps; +} + +/** + * Ask for local-fs path if needed + */ +async function getLocalFsPath( + rl: readline.Interface, + existingPath?: string, +): Promise { + showHeader(); + + console.log(` ${c.purple}${c.bold}Local FS Configuration${c.reset}\n`); + console.log( + ` ${c.gray}The Local FS MCP needs a folder path to mount.${c.reset}`, + ); + console.log( + ` ${c.gray}This is the root folder agents can access.${c.reset}\n`, + ); + + if (existingPath) { + console.log( + ` ${c.gray}Current path: ${c.white}${existingPath}${c.reset}\n`, + ); + } + + while (true) { + const defaultHint = existingPath + ? ` ${c.gray}(${existingPath})${c.reset}` + : ""; + const path = await ask( + rl, + ` ${c.purple}Enter folder path${defaultHint}: ${c.reset}`, + ); + + const finalPath = path || existingPath; + + if (!finalPath) { + console.log(`\n ${c.orange}Path is required.${c.reset}`); + continue; + } + + if (!existsSync(finalPath)) { + console.log(`\n ${c.orange}Path does not exist: ${finalPath}${c.reset}`); + const retry = await ask(rl, ` ${c.gray}Try again? (Y/n): ${c.reset}`); + if (retry.toLowerCase() === "n") { + process.exit(1); + } + continue; + } + + return finalPath; + } +} + +/** + * Show final configuration + */ +function showFinalConfig(config: GatewayConfig): void { + showHeader(); + + console.log(` ${c.brightGreen}${c.bold}✓ Configuration Saved${c.reset}\n`); + console.log(` ${c.gray}File: ${CONFIG_FILE}${c.reset}\n`); + + console.log(` ${c.purple}${c.bold}Selected MCPs:${c.reset}`); + for (const mcpId of config.mcps) { + const mcp = AVAILABLE_MCPS.find((m) => m.id === mcpId); + if (mcp) { + console.log( + ` ${c.brightGreen}✓${c.reset} ${mcp.name} ${c.gray}(port ${mcp.port})${c.reset}`, + ); + } + } + + if (config.localFsPath) { + console.log(`\n ${c.purple}${c.bold}Local FS Path:${c.reset}`); + console.log(` ${c.white}${config.localFsPath}${c.reset}`); + } + + console.log( + `\n ${c.purple}${c.bold}Gateway Port:${c.reset} ${c.white}${config.gatewayPort}${c.reset}`, + ); + + console.log(`\n ${c.gray}${"─".repeat(50)}${c.reset}`); + console.log(`\n ${c.bold}To start the gateway:${c.reset}`); + console.log(` ${c.orange}bun run gateway${c.reset}\n`); +} + +/** + * Main setup flow (exported for use from server.ts) + */ +export async function runSetup(): Promise { + const rl = createRL(); + const existingConfig = loadConfig(); + + try { + // Select MCPs + const selectedMcps = await selectMCPs(rl, existingConfig); + + // Get local-fs path if selected + let localFsPath: string | undefined; + if (selectedMcps.has("local-fs")) { + localFsPath = await getLocalFsPath(rl, existingConfig?.localFsPath); + } + + // Build config + const config: GatewayConfig = { + mcps: Array.from(selectedMcps), + localFsPath, + gatewayPort: existingConfig?.gatewayPort || 8000, + }; + + // Save config + saveConfig(config); + + // Show final config + showFinalConfig(config); + + return config; + } finally { + rl.close(); + } +} + +// Only run main if this is the entry point +const isMainModule = + import.meta.url === `file://${process.argv[1]}` || + process.argv[1]?.endsWith("setup.ts"); + +if (isMainModule) { + runSetup().catch((error) => { + console.error("Setup failed:", error); + process.exit(1); + }); +} + +export { loadConfig, AVAILABLE_MCPS, type GatewayConfig, CONFIG_FILE }; diff --git a/shared/registry.ts b/shared/registry.ts index 67b96f41..089a6ca6 100644 --- a/shared/registry.ts +++ b/shared/registry.ts @@ -96,4 +96,200 @@ export interface Registry extends BindingRegistry { }>; }, ]; + + /** + * Perplexity binding - matches official @perplexity-ai/mcp-server + * + * Tools: perplexity_ask, perplexity_reason, perplexity_research + * All accept messages array with role/content + */ + "@deco/perplexity-ai": [ + { + name: "perplexity_ask"; + inputSchema: z.ZodType<{ + messages: Array<{ role: string; content: string }>; + }>; + outputSchema: z.ZodType<{ + content: string; + citations?: string[]; + }>; + }, + { + name: "perplexity_research"; + inputSchema: z.ZodType<{ + messages: Array<{ role: string; content: string }>; + }>; + outputSchema: z.ZodType<{ + content: string; + citations?: string[]; + }>; + opt?: true; + }, + { + name: "perplexity_reason"; + inputSchema: z.ZodType<{ + messages: Array<{ role: string; content: string }>; + }>; + outputSchema: z.ZodType<{ + content: string; + }>; + opt?: true; + }, + ]; + + /** + * Firecrawl binding - matches official firecrawl-mcp + * + * Tools: firecrawl_scrape, firecrawl_crawl, firecrawl_map, firecrawl_search, etc. + */ + "@deco/firecrawl": [ + { + name: "firecrawl_scrape"; + inputSchema: z.ZodType<{ + url: string; + formats?: string[]; + onlyMainContent?: boolean; + }>; + outputSchema: z.ZodType<{ + success: boolean; + data?: unknown; + error?: string; + }>; + }, + { + name: "firecrawl_crawl"; + inputSchema: z.ZodType<{ + url: string; + maxDepth?: number; + limit?: number; + }>; + outputSchema: z.ZodType<{ + success: boolean; + data?: unknown; + error?: string; + }>; + opt?: true; + }, + { + name: "firecrawl_map"; + inputSchema: z.ZodType<{ + url: string; + search?: string; + limit?: number; + }>; + outputSchema: z.ZodType<{ + success: boolean; + data?: unknown; + error?: string; + }>; + opt?: true; + }, + ]; + + /** + * Local FS binding - matches @decocms/mcp-local-fs (Mesh-style names) + * + * Tools: FILE_READ, FILE_WRITE for basic file operations + */ + "@deco/local-fs": [ + { + name: "FILE_READ"; + inputSchema: z.ZodType<{ + path: string; + encoding?: "utf-8" | "base64"; + }>; + outputSchema: z.ZodType<{ + content: string; + metadata: { + id: string; + title: string; + path: string; + mimeType: string; + size: number; + }; + }>; + }, + { + name: "FILE_WRITE"; + inputSchema: z.ZodType<{ + path: string; + content: string; + encoding?: "utf-8" | "base64"; + createParents?: boolean; + overwrite?: boolean; + }>; + outputSchema: z.ZodType<{ + file: { + id: string; + title: string; + path: string; + mimeType: string; + size: number; + }; + }>; + }, + { + name: "FILE_DELETE"; + inputSchema: z.ZodType<{ + path: string; + recursive?: boolean; + }>; + outputSchema: z.ZodType<{ + success: boolean; + path: string; + }>; + opt?: true; + }, + { + name: "list_directory"; + inputSchema: z.ZodType<{ + path: string; + }>; + outputSchema: z.ZodType; + opt?: true; + }, + ]; + + /** + * MCP Filesystem binding - matches official @modelcontextprotocol/server-filesystem + * + * This is a drop-in compatible binding that works with: + * - @modelcontextprotocol/server-filesystem (official) + * - @decocms/mcp-local-fs (our implementation) + * + * Tools: read_file, write_file, list_directory, create_directory + */ + "@deco/mcp-filesystem": [ + { + name: "read_file"; + inputSchema: z.ZodType<{ + path: string; + }>; + outputSchema: z.ZodType; + }, + { + name: "write_file"; + inputSchema: z.ZodType<{ + path: string; + content: string; + }>; + outputSchema: z.ZodType; + }, + { + name: "list_directory"; + inputSchema: z.ZodType<{ + path: string; + }>; + outputSchema: z.ZodType; + opt?: true; + }, + { + name: "create_directory"; + inputSchema: z.ZodType<{ + path: string; + }>; + outputSchema: z.ZodType; + opt?: true; + }, + ]; } diff --git a/slides/.gitignore b/slides/.gitignore new file mode 100644 index 00000000..babca1bb --- /dev/null +++ b/slides/.gitignore @@ -0,0 +1 @@ +.dev.vars diff --git a/slides/README.md b/slides/README.md new file mode 100644 index 00000000..4a9928d5 --- /dev/null +++ b/slides/README.md @@ -0,0 +1,188 @@ +# Slides MCP + +AI-powered presentation builder that creates beautiful, animated slide decks through natural conversation. + +## Features + +- **Brand-Aware Design Systems** - Create reusable design systems with your brand colors, typography, and logos +- **Multiple Slide Layouts** - Title, content, stats, two-column, list, quote, image, and custom layouts +- **Automatic Brand Research** - Optionally integrate Perplexity and Firecrawl to automatically discover brand assets +- **MCP Apps UI** - Interactive slide viewer and design system preview via MCP Apps resources +- **JSX + Babel** - Modern component-based slides with browser-side transpilation + +## Quick Start + +```bash +# Start the server +bun run dev + +# The MCP will be available at: +# http://localhost:8001/mcp +``` + +## Tools + +### Deck Management +| Tool | Description | +|------|-------------| +| `DECK_INIT` | Initialize a new presentation deck with brand assets | +| `DECK_INFO` | Get information about an existing deck | +| `DECK_BUNDLE` | Bundle a deck for sharing/export | +| `DECK_GET_ENGINE` | Get the presentation engine JSX | +| `DECK_GET_DESIGN_SYSTEM` | Get the design system JSX for a brand | + +### Slide Operations +| Tool | Description | +|------|-------------| +| `SLIDE_CREATE` | Create a new slide with layout and content | +| `SLIDE_UPDATE` | Update an existing slide | +| `SLIDE_DELETE` | Delete a slide | +| `SLIDE_GET` | Get a single slide by ID | +| `SLIDE_LIST` | List all slides in a deck | +| `SLIDE_REORDER` | Reorder slides in a deck | +| `SLIDE_DUPLICATE` | Duplicate an existing slide | +| `SLIDES_PREVIEW` | Preview multiple slides in the viewer | + +### Style Management +| Tool | Description | +|------|-------------| +| `STYLE_GET` | Get the style guide for a brand | +| `STYLE_SET` | Update the style guide | +| `STYLE_SUGGEST` | Get AI suggestions for style improvements | + +### Brand Research (Optional) +| Tool | Description | +|------|-------------| +| `BRAND_RESEARCH` | Automatically discover brand assets from websites | +| `BRAND_RESEARCH_STATUS` | Check which research bindings are available | +| `BRAND_ASSETS_VALIDATE` | Validate and suggest missing brand assets | + +## Prompts + +| Prompt | Description | +|--------|-------------| +| `SLIDES_SETUP_BRAND` | Guide for creating a new brand design system | +| `SLIDES_NEW_DECK` | Create a new presentation deck | +| `SLIDES_ADD_CONTENT` | Add content slides to an existing deck | +| `SLIDES_QUICK_START` | Fast path for simple presentations | +| `SLIDES_LIST` | List available brands and decks | + +## MCP Apps (UI Resources) + +The MCP exposes interactive UI resources for displaying presentations: + +| Resource URI | Description | +|--------------|-------------| +| `ui://slides-viewer` | Full presentation viewer with navigation | +| `ui://design-system` | Brand design system preview | +| `ui://slide` | Single slide preview | + +These resources receive data via the `ui/initialize` message and render interactive HTML/JS applications. + +## Slide Layouts + +### Title Slide +Large background shape with brand accent, bold uppercase title, and logo. + +### Content Slide +Main content with title, sections, bullet points, and footer. + +### Stats Slide +Grid of 3-4 large numbers with labels (e.g., "2,847 Users", "89% Growth"). + +### Two-Column Slide +Side-by-side comparison with column titles and bullets. + +### List Slide +2x2 grid of items with title and description. + +### Quote Slide +Centered quote with attribution. + +### Image Slide +Full background image with overlay text. + +### Custom Slide +Raw HTML content for complete flexibility. + +## Optional Bindings + +Configure these bindings for automatic brand research: + +### Perplexity (`@deco/perplexity`) +- Search for brand logo URLs +- Research brand colors and guidelines +- Find brand taglines and descriptions +- Discover press kits and media pages + +### Firecrawl (`@deco/firecrawl`) +- Extract brand colors from website CSS +- Identify typography and fonts +- Find logo images in page source +- Capture full brand identity from live websites + +When configured, use `BRAND_RESEARCH` before `DECK_INIT` to automatically populate brand assets. + +## File Structure + +``` +~/slides/ +├── brands/ # Reusable design systems +│ └── {brand-name}/ +│ ├── design-system.jsx # Brand components (JSX) +│ ├── styles.css # Brand styles +│ ├── style.md # AI style guide +│ ├── brand-assets.json # Logo URLs, colors +│ └── design.html # Design system viewer +└── decks/ # Presentations + └── {deck-name}/ + ├── index.html # Entry point + ├── engine.jsx # Presentation engine + ├── design-system.jsx # (copied from brand) + ├── styles.css # (copied from brand) + └── slides/ + ├── manifest.json # Slide order and metadata + └── *.json # Individual slides +``` + +## Workflow + +### Phase 1: Brand Setup (one-time) +1. Use `SLIDES_SETUP_BRAND` prompt +2. Optionally run `BRAND_RESEARCH` to auto-discover assets +3. Create design system with `DECK_INIT` +4. Preview and iterate on brand styling + +### Phase 2: Create Presentations +1. Use `SLIDES_NEW_DECK` prompt with existing brand +2. Add slides with `SLIDE_CREATE` +3. Preview with `SLIDES_PREVIEW` +4. Bundle for sharing with `DECK_BUNDLE` + +## Development + +```bash +# Install dependencies +bun install + +# Run development server with hot reload +bun run dev + +# Type check +bun run check + +# Build for production +bun run build +``` + +## Configuration + +The MCP uses the following environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `PORT` | `8001` | Server port | + +## License + +MIT diff --git a/slides/app.json b/slides/app.json new file mode 100644 index 00000000..996a3507 --- /dev/null +++ b/slides/app.json @@ -0,0 +1,19 @@ +{ + "scopeName": "deco", + "name": "slides", + "friendlyName": "Slides", + "connection": { + "type": "HTTP", + "url": "https://sites-slides.decocache.com/mcp" + }, + "description": "AI-powered slide presentation builder with beautiful animations and GSAP transitions.", + "icon": "https://assets.decocache.com/decocms/slides-icon.png", + "unlisted": false, + "metadata": { + "categories": ["Productivity", "Design"], + "official": false, + "tags": ["slides", "presentation", "deck", "gsap", "animations", "react"], + "short_description": "Create beautiful slide presentations through AI conversation.", + "mesh_description": "The Slides MCP enables AI agents to create, edit, and manage beautiful slide presentations through natural conversation. It provides tools for initializing presentation decks, creating slides with various layouts (title, content, stats, two-column, list, quote, image, custom), managing style guides, and bundling presentations into single portable HTML files. The presentation engine uses React and GSAP for smooth animations, requires no build step (CDN-loaded dependencies), and supports responsive scaling. Ideal for investor updates, sales pitches, educational content, conference talks, or any scenario requiring professional presentations." + } +} diff --git a/slides/assets/engine.js b/slides/assets/engine.js new file mode 100644 index 00000000..6a2aec0a --- /dev/null +++ b/slides/assets/engine.js @@ -0,0 +1,716 @@ +/** + * Cogna Presentation Engine + * Styled for Cogna Educação brand guidelines + */ + +const { useState, useEffect, useRef } = React; + +// Base dimensions (16:9 aspect ratio matching Cogna template) +const BASE_WIDTH = 1366; +const BASE_HEIGHT = 768; + +/** + * Cogna Logo Component + */ +function CognaLogo({ size = "normal", className = "" }) { + const isSmall = size === "small"; + + return React.createElement( + "div", + { + className: `logo-cogna ${isSmall ? "logo-small" : ""} ${className}`, + }, + [ + React.createElement( + "span", + { + key: "wordmark", + className: "logo-cogna-wordmark", + }, + [ + "cogn", + React.createElement("span", { key: "dot", className: "dot" }, "a"), + ], + ), + React.createElement( + "span", + { + key: "tagline", + className: "logo-cogna-tagline", + }, + "EDUCAÇÃO", + ), + ], + ); +} + +/** + * Title Slide with blob shapes + */ +function TitleSlide({ slide }) { + return React.createElement( + "div", + { + className: "slide slide--title", + style: { opacity: 1 }, + }, + [ + // Charcoal blob + React.createElement("div", { key: "blob1", className: "blob-primary" }), + // Purple circle + React.createElement("div", { key: "blob2", className: "blob-accent" }), + // Content + React.createElement( + "div", + { key: "content", className: "slide-content" }, + React.createElement("h1", { className: "title-hero" }, slide.title), + ), + // Logo + React.createElement( + "div", + { key: "logo", className: "logo-container" }, + React.createElement(CognaLogo, { size: "normal" }), + ), + ], + ); +} + +/** + * Content Slide with bullets and sections + */ +function ContentSlide({ slide }) { + const items = slide.items || []; + + const renderBullets = (bullets) => { + return React.createElement( + "ul", + { className: "bullet-list" }, + bullets.map((bullet, idx) => + React.createElement( + "li", + { key: idx }, + bullet.highlight + ? React.createElement( + "span", + { className: "text-bold" }, + bullet.text, + ) + : bullet.text, + ), + ), + ); + }; + + const renderNestedBullets = (bullets) => { + return React.createElement( + "ul", + { className: "bullet-list bullet-list--nested" }, + bullets.map((bullet, idx) => + React.createElement("li", { key: idx }, bullet.text), + ), + ); + }; + + return React.createElement( + "div", + { + className: "slide slide--content", + style: { opacity: 1 }, + }, + [ + // Header with logo + React.createElement( + "header", + { key: "header", className: "slide-header" }, + React.createElement(CognaLogo, { size: "small" }), + ), + // Body + React.createElement("main", { key: "body", className: "slide-body" }, [ + // Title + React.createElement( + "h1", + { key: "title", className: "slide-title" }, + slide.title, + ), + // Items + ...items.map((item, idx) => { + const elements = []; + + if (item.title) { + elements.push( + React.createElement( + "h2", + { + key: `section-${idx}`, + className: "section-heading", + }, + item.title, + ), + ); + } + + if (item.bullets) { + elements.push( + React.createElement( + "div", + { key: `bullets-${idx}` }, + renderBullets(item.bullets), + ), + ); + } + + if (item.nestedBullets) { + elements.push( + React.createElement( + "div", + { key: `nested-${idx}` }, + renderNestedBullets(item.nestedBullets), + ), + ); + } + + return React.createElement("div", { key: idx }, elements); + }), + ]), + // Footer + slide.source && + React.createElement( + "footer", + { key: "footer", className: "slide-footer" }, + [ + React.createElement( + "span", + { key: "source", className: "footer-text" }, + `Fonte: ${slide.source}`, + ), + slide.label && + React.createElement( + "span", + { key: "label", className: "footer-label" }, + slide.label, + ), + React.createElement("div", { key: "dot", className: "footer-dot" }), + ], + ), + ], + ); +} + +/** + * Stats Slide with large numbers + */ +function StatsSlide({ slide }) { + const items = slide.items || []; + + return React.createElement( + "div", + { + className: "slide slide--content slide--stats", + style: { opacity: 1 }, + }, + [ + // Header + React.createElement( + "header", + { key: "header", className: "slide-header" }, + React.createElement(CognaLogo, { size: "small" }), + ), + // Title + slide.tag && + React.createElement( + "span", + { + key: "tag", + style: { + fontSize: "12px", + fontWeight: 500, + color: "#9CA3AF", + letterSpacing: "0.1em", + textTransform: "uppercase", + marginBottom: "8px", + }, + }, + slide.tag, + ), + React.createElement( + "h1", + { key: "title", className: "slide-title" }, + slide.title, + ), + // Stats grid + React.createElement( + "div", + { key: "grid", className: "stats-grid" }, + items.map((item, idx) => + React.createElement("div", { key: idx, className: "stat-item" }, [ + React.createElement( + "div", + { key: "value", className: "stat-value" }, + item.value, + ), + React.createElement( + "div", + { key: "label", className: "stat-label" }, + item.label, + ), + ]), + ), + ), + ], + ); +} + +/** + * Two Column Slide + */ +function TwoColumnSlide({ slide }) { + const items = slide.items || []; + const leftItem = items[0]; + const rightItem = items[1]; + + const renderColumn = (item) => { + if (!item) return null; + + return React.createElement("div", { className: "column" }, [ + item.title && + React.createElement( + "h3", + { + key: "title", + className: "column-title", + }, + item.title, + ), + item.bullets && + React.createElement( + "ul", + { + key: "bullets", + className: "bullet-list", + }, + item.bullets.map((bullet, idx) => + React.createElement( + "li", + { key: idx }, + bullet.highlight + ? React.createElement( + "span", + { className: "text-bold" }, + bullet.text, + ) + : bullet.text, + ), + ), + ), + ]); + }; + + return React.createElement( + "div", + { + className: "slide slide--content slide--two-column", + style: { opacity: 1 }, + }, + [ + // Header + React.createElement( + "header", + { key: "header", className: "slide-header" }, + React.createElement(CognaLogo, { size: "small" }), + ), + // Body + React.createElement("main", { key: "body", className: "slide-body" }, [ + slide.tag && + React.createElement( + "span", + { + key: "tag", + style: { + fontSize: "12px", + fontWeight: 500, + color: "#9CA3AF", + letterSpacing: "0.1em", + textTransform: "uppercase", + marginBottom: "8px", + display: "block", + }, + }, + slide.tag, + ), + React.createElement( + "h1", + { key: "title", className: "slide-title" }, + slide.title, + ), + React.createElement("div", { key: "columns", className: "columns" }, [ + React.createElement("div", { key: "left" }, renderColumn(leftItem)), + React.createElement("div", { key: "right" }, renderColumn(rightItem)), + ]), + ]), + ], + ); +} + +/** + * List Slide with 2x2 grid + */ +function ListSlide({ slide }) { + const items = slide.items || []; + + return React.createElement( + "div", + { + className: "slide slide--content slide--list", + style: { opacity: 1 }, + }, + [ + // Header + React.createElement( + "header", + { key: "header", className: "slide-header" }, + React.createElement(CognaLogo, { size: "small" }), + ), + // Body + React.createElement("main", { key: "body", className: "slide-body" }, [ + slide.tag && + React.createElement( + "span", + { + key: "tag", + style: { + fontSize: "12px", + fontWeight: 500, + color: "#9CA3AF", + letterSpacing: "0.1em", + textTransform: "uppercase", + marginBottom: "8px", + display: "block", + }, + }, + slide.tag, + ), + React.createElement( + "h1", + { key: "title", className: "slide-title" }, + slide.title, + ), + slide.subtitle && + React.createElement( + "p", + { + key: "subtitle", + style: { + fontSize: "16px", + color: "#6B7280", + marginBottom: "16px", + }, + }, + slide.subtitle, + ), + React.createElement( + "div", + { key: "grid", className: "list-grid" }, + items.map((item, idx) => + React.createElement("div", { key: idx, className: "list-item" }, [ + React.createElement("div", { + key: "dot", + className: "list-item-dot", + }), + React.createElement( + "div", + { key: "content", className: "list-item-content" }, + [ + React.createElement("h4", { key: "title" }, item.title), + item.subtitle && + React.createElement("p", { key: "sub" }, item.subtitle), + ], + ), + ]), + ), + ), + ]), + ], + ); +} + +/** + * Comparison Box Component (for content slides) + */ +function ComparisonBox({ columns }) { + return React.createElement( + "div", + { className: "comparison-box" }, + columns.map((col, idx) => { + // Check if this is an operator + if (col.operator) { + return React.createElement( + "div", + { + key: idx, + className: "comparison-box__operator", + }, + col.operator, + ); + } + + return React.createElement( + "div", + { + key: idx, + className: "comparison-box__column", + }, + [ + React.createElement( + "h4", + { + key: "title", + className: "comparison-box__title", + }, + col.title, + ), + col.items && + React.createElement( + "ul", + { + key: "list", + className: "comparison-box__list", + }, + col.items.map((item, i) => + React.createElement("li", { key: i }, item), + ), + ), + col.note && + React.createElement( + "span", + { + key: "note", + className: "comparison-box__note", + }, + col.note, + ), + ], + ); + }), + ); +} + +/** + * Main Presentation Component + */ +function Presentation({ slides, title, subtitle }) { + const [currentSlide, setCurrentSlide] = useState(0); + const [isAnimating, setIsAnimating] = useState(false); + const [scale, setScale] = useState(1); + const containerRef = useRef(null); + + // Calculate scale to fit viewport + useEffect(() => { + const calculateScale = () => { + if (!containerRef.current) return; + const container = containerRef.current; + const scaleX = container.clientWidth / BASE_WIDTH; + const scaleY = container.clientHeight / BASE_HEIGHT; + setScale(Math.min(scaleX, scaleY) * 0.95); + }; + + calculateScale(); + window.addEventListener("resize", calculateScale); + return () => window.removeEventListener("resize", calculateScale); + }, []); + + // Keyboard navigation + useEffect(() => { + const handleKeyDown = (e) => { + if (isAnimating) return; + + if (e.key === "ArrowRight" || e.key === "ArrowDown" || e.key === " ") { + e.preventDefault(); + goToSlide(Math.min(currentSlide + 1, slides.length - 1)); + } else if (e.key === "ArrowLeft" || e.key === "ArrowUp") { + e.preventDefault(); + goToSlide(Math.max(currentSlide - 1, 0)); + } else if (e.key === "Home") { + e.preventDefault(); + goToSlide(0); + } else if (e.key === "End") { + e.preventDefault(); + goToSlide(slides.length - 1); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [currentSlide, isAnimating, slides.length]); + + const goToSlide = (index) => { + if (index === currentSlide || isAnimating) return; + setIsAnimating(true); + setCurrentSlide(index); + setTimeout(() => setIsAnimating(false), 500); + }; + + const renderSlide = (slide, index) => { + const isActive = index === currentSlide; + + if (!isActive) return null; + + const slideStyle = { + transform: `scale(${scale})`, + opacity: 1, + }; + + let SlideComponent; + switch (slide.layout) { + case "title": + SlideComponent = TitleSlide; + break; + case "stats": + SlideComponent = StatsSlide; + break; + case "two-column": + SlideComponent = TwoColumnSlide; + break; + case "list": + SlideComponent = ListSlide; + break; + case "content": + default: + SlideComponent = ContentSlide; + break; + } + + return React.createElement( + "div", + { + key: slide.id, + style: slideStyle, + }, + React.createElement(SlideComponent, { slide }), + ); + }; + + return React.createElement( + "div", + { + ref: containerRef, + className: "presentation-container", + }, + [ + // Slides + ...slides.map(renderSlide), + + // Navigation controls + React.createElement("div", { key: "nav", className: "nav-controls" }, [ + // Slide indicator + React.createElement( + "span", + { key: "indicator", className: "nav-indicator" }, + `${String(currentSlide + 1).padStart(2, "0")} / ${String(slides.length).padStart(2, "0")}`, + ), + // First slide button + React.createElement( + "button", + { + key: "first", + className: "nav-btn", + onClick: () => goToSlide(0), + disabled: currentSlide === 0 || isAnimating, + title: "First slide", + }, + "⏮", + ), + // Previous button + React.createElement( + "button", + { + key: "prev", + className: "nav-btn", + onClick: () => goToSlide(currentSlide - 1), + disabled: currentSlide === 0 || isAnimating, + title: "Previous slide", + }, + "←", + ), + // Next button + React.createElement( + "button", + { + key: "next", + className: "nav-btn nav-btn--primary", + onClick: () => goToSlide(currentSlide + 1), + disabled: currentSlide === slides.length - 1 || isAnimating, + title: "Next slide", + }, + "→", + ), + // Fullscreen button + React.createElement( + "button", + { + key: "fs", + className: "nav-btn", + onClick: () => { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else { + document.documentElement.requestFullscreen(); + } + }, + title: "Toggle fullscreen", + }, + "⛶", + ), + ]), + ], + ); +} + +/** + * Initialize presentation from manifest + */ +async function initPresentation(manifestPath = "./slides/manifest.json") { + try { + const response = await fetch(manifestPath); + const manifest = await response.json(); + + // Load all slides + const slides = await Promise.all( + manifest.slides.map(async (slideInfo) => { + try { + const slideResponse = await fetch(`./slides/${slideInfo.file}`); + const slideData = await slideResponse.json(); + return { ...slideInfo, ...slideData }; + } catch (e) { + console.error(`Failed to load slide: ${slideInfo.file}`, e); + return slideInfo; + } + }), + ); + + // Render presentation + const root = ReactDOM.createRoot(document.getElementById("root")); + root.render( + React.createElement(Presentation, { + slides, + title: manifest.title || "Presentation", + subtitle: manifest.subtitle || "", + }), + ); + } catch (e) { + console.error("Failed to initialize presentation:", e); + document.getElementById("root").innerHTML = ` +
+

Presentation not found

+

+ Create a manifest.json in the slides directory to get started. +

+
+ `; + } +} + +// Export for global access +window.Presentation = Presentation; +window.initPresentation = initPresentation; +window.CognaLogo = CognaLogo; +window.ComparisonBox = ComparisonBox; diff --git a/slides/assets/index.html b/slides/assets/index.html new file mode 100644 index 00000000..611f5bab --- /dev/null +++ b/slides/assets/index.html @@ -0,0 +1,58 @@ + + + + + + {{TITLE}} + + + + + + + + + + + + + + + + + +
+
Loading presentation
+
+ + + + diff --git a/slides/assets/manifest-template.json b/slides/assets/manifest-template.json new file mode 100644 index 00000000..456d934a --- /dev/null +++ b/slides/assets/manifest-template.json @@ -0,0 +1,7 @@ +{ + "title": "Presentation Title", + "subtitle": "Subtitle or date", + "createdAt": "", + "updatedAt": "", + "slides": [] +} diff --git a/slides/assets/slide-template.json b/slides/assets/slide-template.json new file mode 100644 index 00000000..5342cff2 --- /dev/null +++ b/slides/assets/slide-template.json @@ -0,0 +1,13 @@ +{ + "id": "", + "layout": "title", + "title": "", + "subtitle": "", + "tag": "", + "backgroundColor": "dc-950", + "textColor": "light", + "accent": "green", + "items": [], + "backgroundImage": "", + "customHtml": "" +} diff --git a/slides/assets/style-template.md b/slides/assets/style-template.md new file mode 100644 index 00000000..132b71e9 --- /dev/null +++ b/slides/assets/style-template.md @@ -0,0 +1,52 @@ +# Presentation Style Guide + +This document defines the visual style, tone, and design system for this presentation. + +## Brand Colors + +- **Primary Accent**: Green (#c4df1b) - Used for emphasis and key points +- **Secondary Accent**: Purple (#d4a5ff) - Used for alternative sections +- **Tertiary Accent**: Yellow (#ffd666) - Used for highlights + +## Background Theme + +- **Primary Background**: Dark (#0f0e0d) - Main slide background +- **Secondary Background**: Slightly lighter dark (#1a1918) +- **Accent Backgrounds**: Green, purple, or yellow for section breaks + +## Typography + +- **Headlines**: Large, bold, with tight letter-spacing (-4px for main titles) +- **Body Text**: Clean, readable, 17-18px for content +- **Monospace**: Used for tags, labels, and technical content +- **Uppercase Tags**: Small, tracking-wide, for section labels + +## Layout Principles + +1. **Generous Whitespace**: Slides should breathe, avoid clutter +2. **Clear Hierarchy**: Use size and color to establish importance +3. **Consistent Padding**: 80-96px padding on content slides +4. **Grid-Based**: Use 2-column or multi-column grids for complex content + +## Tone & Voice + +- Professional but approachable +- Data-driven when presenting statistics +- Clear, concise language +- Action-oriented conclusions + +## Slide Types to Use + +- **Title Slides**: For section introductions +- **Content Slides**: For detailed information with bullet points +- **Stats Slides**: For numerical data with animated counters +- **Two-Column**: For comparisons or side-by-side information +- **List Slides**: For action items or key points +- **Quote Slides**: For testimonials or important statements +- **Image Slides**: For visual impact + +## Animation Style + +- **Subtle**: Smooth fade-in with slight upward movement +- **Staggered**: Elements appear sequentially for better readability +- **Count-Up**: Numbers animate from 0 for impact diff --git a/slides/assets/styles.css b/slides/assets/styles.css new file mode 100644 index 00000000..3f4d52a8 --- /dev/null +++ b/slides/assets/styles.css @@ -0,0 +1,549 @@ +/* Cogna Educação Presentation Styles */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + +:root { + /* Cogna Brand Colors */ + --cogna-purple: #8B5CF6; + --cogna-purple-light: #A78BFA; + --cogna-purple-pale: #EDE9FE; + + /* Neutrals */ + --charcoal: #3D3D3D; + --charcoal-dark: #2D2D2D; + --gray-light: #E5E5E5; + --gray-medium: #9CA3AF; + --white: #FFFFFF; + --black: #1A1A1A; + + /* Semantic */ + --text-primary: #1A1A1A; + --text-secondary: #6B7280; + --text-on-dark: #FFFFFF; + --border-color: #E5E5E5; + + /* Slide dimensions */ + --slide-width: 1366px; + --slide-height: 768px; + --margin-x: 48px; + --margin-y: 40px; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } + +html, body { + width: 100%; + height: 100%; + overflow: hidden; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: #1a1a1a; + -webkit-font-smoothing: antialiased; +} + +#root { width: 100%; height: 100%; } + +/* Presentation Container */ +.presentation-container { + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: #1a1a1a; + position: relative; +} + +/* Slide Base */ +.slide { + width: var(--slide-width); + height: var(--slide-height); + position: absolute; + overflow: hidden; + transform-origin: center center; +} + +/* ============================================ + TITLE SLIDE - With blob shapes + ============================================ */ +.slide--title { + background: var(--gray-light); + position: relative; +} + +/* Large charcoal blob - left side with curved right edge */ +.blob-primary { + position: absolute; + width: 70%; + height: 130%; + background: var(--charcoal); + border-radius: 0 50% 50% 0; + left: 0; + top: -15%; + z-index: 1; +} + +/* Purple circle - overlapping on right */ +.blob-accent { + position: absolute; + width: 380px; + height: 380px; + background: var(--cogna-purple); + border-radius: 50%; + right: 12%; + top: -8%; + z-index: 2; +} + +.slide--title .slide-content { + position: relative; + z-index: 3; + padding: 0 var(--margin-x); + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; +} + +.slide--title .title-hero { + font-size: 72px; + font-weight: 700; + color: var(--white); + line-height: 1.1; + letter-spacing: -0.02em; + text-transform: uppercase; + max-width: 60%; +} + +.slide--title .logo-container { + position: absolute; + bottom: var(--margin-y); + right: var(--margin-x); + z-index: 3; +} + +/* Cogna Logo */ +.logo-cogna { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.logo-cogna-wordmark { + font-family: 'Inter', sans-serif; + font-size: 42px; + font-weight: 700; + color: var(--charcoal); + letter-spacing: -0.02em; + line-height: 1; +} + +.logo-cogna-wordmark .dot { + color: var(--cogna-purple); +} + +.logo-cogna-tagline { + font-family: 'Inter', sans-serif; + font-size: 11px; + font-weight: 500; + color: var(--charcoal); + letter-spacing: 0.2em; + text-transform: uppercase; + margin-top: 2px; +} + +/* ============================================ + CONTENT SLIDE - White background + ============================================ */ +.slide--content { + background: var(--white); + padding: var(--margin-y) var(--margin-x); + display: flex; + flex-direction: column; +} + +.slide--content .slide-header { + display: flex; + justify-content: flex-end; + margin-bottom: 16px; + flex-shrink: 0; +} + +.slide--content .logo-small { + height: 36px; +} + +.slide--content .logo-small .logo-cogna-wordmark { + font-size: 28px; +} + +.slide--content .logo-small .logo-cogna-tagline { + font-size: 8px; +} + +.slide--content .slide-body { + flex: 1; + overflow: hidden; +} + +.slide--content .slide-title { + font-size: 36px; + font-weight: 700; + color: var(--cogna-purple); + line-height: 1.2; + margin-bottom: 20px; +} + +.slide--content .section-heading { + font-size: 24px; + font-weight: 600; + color: var(--cogna-purple); + line-height: 1.3; + margin-top: 28px; + margin-bottom: 16px; +} + +.slide--content .body-text { + font-size: 16px; + font-weight: 400; + color: var(--text-primary); + line-height: 1.6; +} + +/* Bullet Lists */ +.bullet-list { + list-style: none; + padding: 0; + margin: 0 0 16px 0; +} + +.bullet-list > li { + position: relative; + padding-left: 24px; + margin-bottom: 14px; + font-size: 16px; + line-height: 1.5; + color: var(--text-primary); +} + +.bullet-list > li::before { + content: ''; + position: absolute; + left: 0; + top: 8px; + width: 8px; + height: 8px; + background: var(--cogna-purple); + border-radius: 50%; +} + +/* Nested bullets - hollow circles */ +.bullet-list--nested { + margin-left: 32px; + margin-top: 10px; + margin-bottom: 10px; +} + +.bullet-list--nested > li::before { + background: transparent; + border: 2px solid var(--cogna-purple); + width: 6px; + height: 6px; + top: 7px; +} + +/* Bold purple text */ +.text-bold { + font-weight: 600; + color: var(--cogna-purple); +} + +/* ============================================ + COMPARISON BOX COMPONENT + ============================================ */ +.comparison-box { + display: flex; + align-items: stretch; + gap: 0; + margin: 24px 0; + background: var(--white); + border: 1px solid var(--border-color); + border-radius: 8px; + overflow: hidden; +} + +.comparison-box__column { + flex: 1; + padding: 20px 24px; + border-right: 1px solid var(--border-color); +} + +.comparison-box__column:last-child { + border-right: none; +} + +.comparison-box__title { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 12px; +} + +.comparison-box__list { + list-style: disc; + padding-left: 20px; + margin: 0; +} + +.comparison-box__list li { + font-size: 14px; + color: var(--cogna-purple); + margin-bottom: 4px; +} + +.comparison-box__note { + font-size: 13px; + color: var(--cogna-purple); + margin-top: 8px; +} + +.comparison-box__operator { + display: flex; + align-items: center; + justify-content: center; + padding: 0 16px; + font-size: 16px; + color: var(--text-secondary); + background: var(--gray-light); + min-width: 50px; +} + +/* ============================================ + SLIDE FOOTER + ============================================ */ +.slide-footer { + display: flex; + align-items: center; + gap: 16px; + padding-top: 16px; + margin-top: auto; + flex-shrink: 0; + position: relative; +} + +.footer-text { + font-size: 11px; + font-weight: 400; + color: var(--text-secondary); +} + +.footer-label { + font-size: 11px; + font-weight: 500; + color: var(--text-secondary); +} + +.footer-dot { + position: absolute; + right: 0; + bottom: 0; + width: 40px; + height: 40px; + background: var(--cogna-purple-light); + border-radius: 50%; + opacity: 0.7; +} + +/* ============================================ + STATS SLIDE + ============================================ */ +.slide--stats { + background: var(--white); + padding: var(--margin-y) var(--margin-x); + display: flex; + flex-direction: column; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 32px; + margin-top: auto; + margin-bottom: auto; +} + +.stat-item { + text-align: center; +} + +.stat-value { + font-size: 64px; + font-weight: 700; + color: var(--cogna-purple); + line-height: 1; + margin-bottom: 8px; +} + +.stat-label { + font-size: 16px; + font-weight: 500; + color: var(--text-secondary); +} + +/* ============================================ + TWO COLUMN LAYOUT + ============================================ */ +.slide--two-column .columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 48px; + margin-top: 24px; +} + +.slide--two-column .column-title { + font-size: 20px; + font-weight: 600; + color: var(--cogna-purple); + margin-bottom: 16px; +} + +/* ============================================ + LIST SLIDE + ============================================ */ +.slide--list .list-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 32px 48px; + margin-top: 32px; +} + +.list-item { + display: flex; + align-items: flex-start; + gap: 16px; +} + +.list-item-dot { + width: 10px; + height: 10px; + background: var(--cogna-purple); + border-radius: 50%; + flex-shrink: 0; + margin-top: 6px; +} + +.list-item-content h4 { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; +} + +.list-item-content p { + font-size: 14px; + color: var(--text-secondary); + line-height: 1.4; +} + +/* ============================================ + NAVIGATION CONTROLS + ============================================ */ +.nav-controls { + position: fixed; + bottom: 24px; + right: 24px; + display: flex; + align-items: center; + gap: 12px; + z-index: 100; +} + +.nav-indicator { + font-size: 13px; + color: rgba(255, 255, 255, 0.5); + font-variant-numeric: tabular-nums; + margin-right: 8px; +} + +.nav-btn { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: rgba(255, 255, 255, 0.8); + cursor: pointer; + transition: all 0.2s; + font-size: 16px; +} + +.nav-btn:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.3); +} + +.nav-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.nav-btn--primary { + background: var(--cogna-purple); + border-color: var(--cogna-purple); + color: white; +} + +.nav-btn--primary:hover:not(:disabled) { + background: var(--cogna-purple-light); + border-color: var(--cogna-purple-light); +} + +/* ============================================ + ANIMATIONS + ============================================ */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-item { + animation: fadeInUp 0.5s ease-out forwards; +} + +/* Loading state */ +.loading { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + color: var(--cogna-purple); + font-family: 'Inter', sans-serif; + font-size: 16px; +} + +.loading::after { + content: ''; + width: 24px; + height: 24px; + margin-left: 12px; + border: 2px solid var(--gray-light); + border-top-color: var(--cogna-purple); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Fullscreen */ +:fullscreen { background: #1a1a1a; } +:-webkit-full-screen { background: #1a1a1a; } +:-moz-full-screen { background: #1a1a1a; } diff --git a/slides/package.json b/slides/package.json new file mode 100644 index 00000000..e14e2fd0 --- /dev/null +++ b/slides/package.json @@ -0,0 +1,32 @@ +{ + "name": "@decocms/slides", + "version": "1.0.0", + "description": "AI-powered slide presentation builder with beautiful animations", + "private": true, + "type": "module", + "scripts": { + "dev": "bun run --hot server/main.ts", + "build:server": "NODE_ENV=production bun build server/main.ts --target=bun --outfile=dist/server/main.js", + "build": "bun run build:server", + "publish": "cat app.json | deco registry publish -w /shared/deco -y", + "check": "tsc --noEmit" + }, + "exports": { + "./tools": "./server/tools/index.ts" + }, + "dependencies": { + "@decocms/bindings": "^1.0.9", + "@decocms/runtime": "1.2.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "@decocms/mcps-shared": "1.0.0", + "@mastra/core": "^0.24.0", + "@modelcontextprotocol/sdk": "1.25.1", + "deco-cli": "^0.28.0", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/slides/server/main.ts b/slides/server/main.ts new file mode 100644 index 00000000..c9955437 --- /dev/null +++ b/slides/server/main.ts @@ -0,0 +1,118 @@ +/** + * Slides MCP - AI-Powered Presentation Builder + * + * Create beautiful, animated slide decks through natural conversation. + * + * ## Features + * + * - **Brand-Aware Design Systems** - Create reusable design systems with brand colors, typography, and logos + * - **Multiple Slide Layouts** - Title, content, stats, two-column, list, quote, image, and custom + * - **Brand MCP Integration** - Connect to Brand MCP for automatic brand discovery + * - **MCP Apps UI** - Interactive slide viewer and design system preview + * - **JSX + Babel** - Modern component-based slides with browser-side transpilation + */ +import { withRuntime } from "@decocms/runtime"; +import { tools } from "./tools/index.ts"; +import { prompts } from "./prompts.ts"; +import { resources } from "./resources/index.ts"; +import { StateSchema, type Env, type Registry } from "./types/env.ts"; + +export { StateSchema }; + +const PORT = process.env.PORT || 8004; + +console.log("[slides-mcp] Starting server..."); +console.log("[slides-mcp] Port:", PORT); +console.log("[slides-mcp] Tools count:", tools.length); +console.log("[slides-mcp] Prompts count:", prompts.length); +console.log("[slides-mcp] Resources count:", resources.length); + +const runtime = withRuntime({ + configuration: { + scopes: ["BRAND::*"], + state: StateSchema, + }, + tools, + prompts, + resources, +}); + +console.log("[slides-mcp] Runtime initialized"); + +/** + * Fetch handler with logging + */ +const fetchWithLogging = async (req: Request): Promise => { + const url = new URL(req.url); + const startTime = Date.now(); + + // Log incoming request + if (req.method === "POST" && url.pathname === "/mcp") { + try { + const body = await req.clone().json(); + const method = body?.method || "unknown"; + const toolName = body?.params?.name; + + if (method === "tools/call" && toolName) { + console.log(`[slides-mcp] 🔧 Tool call: ${toolName}`); + } else if (method !== "unknown") { + console.log(`[slides-mcp] 📨 Request: ${method}`); + } + } catch { + // Ignore JSON parse errors + } + } + + // Call the runtime + const response = await runtime.fetch(req); + + // Log response time for tool calls + const duration = Date.now() - startTime; + if (duration > 100) { + console.log(`[slides-mcp] ⏱️ Response in ${duration}ms`); + } + + return response; +}; + +// Start the server +Bun.serve({ + port: PORT, + hostname: "0.0.0.0", + idleTimeout: 0, // Required for SSE + fetch: fetchWithLogging, + development: process.env.NODE_ENV !== "production", +}); + +console.log(""); +console.log("🎯 Slides MCP running at: http://localhost:" + PORT + "/mcp"); +console.log(""); +console.log("[slides-mcp] Available tools:"); +console.log(" - DECK_INIT - Initialize a new presentation"); +console.log(" - DECK_GET - Get current deck state"); +console.log(" - SLIDE_CREATE - Add slides to presentation"); +console.log(" - SLIDE_UPDATE - Modify existing slides"); +console.log(" - SLIDE_DELETE - Remove slides"); +console.log(" - SLIDES_PREVIEW - Preview multiple slides"); +console.log(""); +console.log("[slides-mcp] MCP Apps (UI Resources):"); +console.log(" - ui://slides-viewer - Full presentation viewer"); +console.log(" - ui://design-system - Brand design system preview"); +console.log(" - ui://slide - Single slide preview"); +console.log(""); +console.log("[slides-mcp] Optional binding: BRAND (Brand MCP)"); +console.log(" Connect Brand MCP for automatic brand discovery"); + +// Copy URL to clipboard on macOS +if (process.platform === "darwin") { + try { + const proc = Bun.spawn(["pbcopy"], { + stdin: "pipe", + }); + proc.stdin.write(`http://localhost:${PORT}/mcp`); + proc.stdin.end(); + console.log("[slides-mcp] 📋 MCP URL copied to clipboard!"); + } catch { + // Ignore clipboard errors + } +} diff --git a/slides/server/prompts.ts b/slides/server/prompts.ts new file mode 100644 index 00000000..ac118abd --- /dev/null +++ b/slides/server/prompts.ts @@ -0,0 +1,653 @@ +/** + * Slides MCP Prompts + * + * These prompts guide the workflow for creating presentations. + * They work WITH the tools to establish a clear flow: + * + * ## File Structure + * + * ~/slides/ # Root workspace (configurable) + * ├── brands/ # Reusable design systems + * │ ├── {brand-name}/ + * │ │ ├── design-system.jsx # Brand components (JSX) + * │ │ ├── styles.css # Brand styles + * │ │ ├── style.md # AI style guide + * │ │ └── design.html # Design system viewer + * │ └── ... + * └── decks/ # Presentations + * ├── {deck-name}/ + * │ ├── index.html # Entry point + * │ ├── engine.jsx # Presentation engine + * │ ├── design-system.jsx # (copied from brand) + * │ ├── styles.css # (copied from brand) + * │ └── slides/ + * │ ├── manifest.json + * │ └── *.json # Individual slides + * └── ... + * + * ## Workflow + * + * Phase 1: SETUP (one-time per brand) + * 1. SLIDES_SETUP_BRAND - Research brand and create design system + * 2. Show design system viewer to user for approval + * 3. Save approved design system in brands/{brand}/ + * + * Phase 2: CREATE (per presentation) + * 1. SLIDES_NEW_DECK - Create a new deck using saved design system + * 2. SLIDES_ADD_CONTENT - Add slides with content + */ + +// Default workspace location +const DEFAULT_WORKSPACE = "~/slides"; + +import { createPrompt, type GetPromptResult } from "@decocms/runtime"; +import { z } from "zod"; +import type { Env } from "./types/env.ts"; + +/** + * SLIDES_SETUP_BRAND - Research and create a brand design system + */ +export const createSetupBrandPrompt = (_env: Env) => + createPrompt({ + name: "SLIDES_SETUP_BRAND", + title: "Setup Brand Design System", + description: `Create a new brand design system for presentations. This is the FIRST step for a new brand. + +The agent should: +1. Research the brand (website, existing materials, style guides) +2. Extract colors, typography, logo treatment, and visual style +3. **COLLECT BRAND ASSETS** (logo images are essential!) +4. Create a customized design system +5. Generate sample slides showing all layouts +6. Show the design system viewer (/design.html) for user approval + +Files are saved to: {workspace}/brands/{brand-slug}/ +Once approved, the design system can be reused for all future presentations.`, + argsSchema: { + brandName: z + .string() + .describe("Company or brand name (e.g., 'Acme Corp')"), + brandWebsite: z + .string() + .optional() + .describe("Brand website URL for research (e.g., 'https://acme.com')"), + logoUrl: z + .string() + .optional() + .describe( + "Primary logo image URL (horizontal format, PNG/SVG preferred)", + ), + logoLightUrl: z + .string() + .optional() + .describe("Light version of logo for dark backgrounds"), + logoDarkUrl: z + .string() + .optional() + .describe("Dark version of logo for light backgrounds"), + iconUrl: z.string().optional().describe("Square icon/favicon URL"), + styleNotes: z + .string() + .optional() + .describe("Any specific style notes or preferences"), + workspace: z + .string() + .optional() + .describe("Workspace root (default: ~/slides)"), + }, + execute: ({ args }): GetPromptResult => { + const { + brandName, + brandWebsite, + styleNotes, + logoUrl, + logoLightUrl, + logoDarkUrl, + iconUrl, + } = args; + const workspace = args.workspace || DEFAULT_WORKSPACE; + const brandSlug = (brandName || "brand") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/-+$/, ""); + const brandPath = `${workspace}/brands/${brandSlug}`; + + const hasProvidedAssets = Boolean(logoUrl); + + return { + description: `Create a design system for ${brandName}`, + messages: [ + { + role: "user", + content: { + type: "text", + text: `# Create Brand Design System: ${brandName} + +## File Structure +This will create a reusable brand at: +\`\`\` +${brandPath}/ +├── design-system.jsx # Brand components (logo, slides) +├── styles.css # Brand colors, typography +├── style.md # AI style guide +├── brand-assets.json # Asset configuration +└── design.html # Component viewer +\`\`\` + +## Your Task +Create a complete presentation design system for **${brandName}**. + +## CRITICAL: Brand Assets Required + +Professional presentations need proper brand assets. ${ + hasProvidedAssets + ? ` +✅ Logo provided: ${logoUrl}${logoLightUrl ? `\n✅ Light logo: ${logoLightUrl}` : ""}${logoDarkUrl ? `\n✅ Dark logo: ${logoDarkUrl}` : ""}${iconUrl ? `\n✅ Icon: ${iconUrl}` : ""}` + : ` +⚠️ **No logo images provided!** + +### STEP 1: Try Automatic Brand Research (Recommended) + +First, check if research bindings are available: +\`\`\` +Call BRAND_RESEARCH_STATUS to check available bindings +\`\`\` + +If PERPLEXITY or FIRECRAWL bindings are configured, use automatic research: +\`\`\` +Call BRAND_RESEARCH with: +- brandName: "${brandName || "brand"}" +- websiteUrl: "${brandWebsite || "(brand website URL)"}" +\`\`\` + +This will automatically discover: +- Logo image URLs +- Brand colors (hex values) +- Typography/fonts +- Brand tagline and description + +### STEP 2: Manual Collection (if no bindings or research incomplete) + +If automatic research is unavailable or incomplete, collect manually: + +1. **Primary Logo** (REQUIRED for professional brands): + - Horizontal/wide format preferred + - PNG with transparent background or SVG + - URL or file path + +2. **Light Logo** (optional but recommended): + - White or light-colored version + - For use on dark backgrounds (title slides) + +3. **Dark Logo** (optional): + - Black or dark-colored version + - For use on light backgrounds (content slides) + +4. **Icon** (optional): + - Square format (1:1 ratio) + - For favicon and small spaces + +### How to Get Assets Manually +- Ask the user directly: "Please provide your logo image URL or file" +- If they have a website, look for logos in: + - \`/logo.png\`, \`/logo.svg\` + - \`/images/logo-*\` + - The \`\` tag + - The favicon (\`/favicon.ico\`, \`/favicon.png\`) +- Extract from their brand guidelines PDF if provided + +**DO NOT proceed without at least a primary logo URL!**` + } + +## Research Phase +${ + brandWebsite + ? `1. Visit ${brandWebsite} to understand the brand identity +2. **Find logo images** (check /logo.png, favicon, og:image) +3. Extract: primary colors, secondary colors, typography, visual style` + : "1. Ask the user for brand colors, fonts, and style preferences\n2. **Request logo image URLs** (this is essential!)" +} +${styleNotes ? `\nUser notes: ${styleNotes}` : ""} + +## Creation Phase + +### Step 1: Ensure workspace exists +\`\`\`bash +mkdir -p ${brandPath} +\`\`\` + +### Step 2: Generate brand files +Call \`DECK_INIT\` with: +- title: "${brandName} Design System" +- brandName: "${brandName}" +- brandTagline: (extract from research or ask user) +- brandColor: (primary brand color from research) +- assets: { + logoUrl: "${logoUrl || "(URL from research or user)"}", + logoLightUrl: "${logoLightUrl || "(optional - for dark backgrounds)"}", + logoDarkUrl: "${logoDarkUrl || "(optional - for light backgrounds)"}", + iconUrl: "${iconUrl || "(optional - for favicon)"}" + } + +### Step 3: Save brand files +From DECK_INIT output, write these to ${brandPath}/: +- design-system.jsx +- styles.css +- style.md +- design.html +- brand-assets.json + +(Do NOT save index.html, engine.jsx, or slides/ - those go in decks) + +### Step 4: Start preview server +\`\`\`bash +cd ${brandPath} && npx serve +\`\`\` +(Or: \`python -m http.server 8890\`) + +### Step 5: Show design system viewer +Navigate to http://localhost:8890/design.html + +Ask: "Here's the design system for ${brandName}. Does this match your brand?" + +## Iteration +If changes needed: +- Edit design-system.jsx for component changes +- Edit styles.css for colors/typography +- Refresh design.html to preview + +## Completion +When approved: +"✓ Brand '${brandName}' saved to ${brandPath}/ +You can now create presentations with: SLIDES_NEW_DECK(brand: '${brandSlug}')"`, + }, + }, + ], + }; + }, + }); + +/** + * SLIDES_NEW_DECK - Create a new presentation using an existing design system + */ +export const createNewDeckPrompt = (_env: Env) => + createPrompt({ + name: "SLIDES_NEW_DECK", + title: "Create New Presentation", + description: `Create a new slide deck using an existing brand design system. + +Prerequisites: +- Brand design system already created (via SLIDES_SETUP_BRAND) +- Brand exists at {workspace}/brands/{brand}/ + +The agent will: +1. Copy the design system from the brand +2. Initialize a new deck with the presentation title +3. Create slides based on user content +4. Preview and iterate until satisfied`, + argsSchema: { + title: z + .string() + .describe("Presentation title (e.g., 'Q4 2025 Results')"), + brand: z + .string() + .describe("Brand slug (e.g., 'acme' - must exist in brands/)"), + deckName: z + .string() + .optional() + .describe("Deck folder name (default: generated from title)"), + outline: z + .string() + .optional() + .describe("Optional slide outline or key points to cover"), + workspace: z + .string() + .optional() + .describe("Workspace root (default: ~/slides)"), + }, + execute: ({ args }): GetPromptResult => { + const { title, brand, outline } = args; + const workspace = args.workspace || DEFAULT_WORKSPACE; + const deckSlug = + args.deckName || + (title || "presentation") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/-+$/, ""); + const brandPath = `${workspace}/brands/${brand}`; + const deckPath = `${workspace}/decks/${deckSlug}`; + + return { + description: `Create presentation: ${title}`, + messages: [ + { + role: "user", + content: { + type: "text", + text: `# Create New Presentation: ${title} + +## File Structure +\`\`\` +${deckPath}/ +├── index.html # Entry point +├── engine.jsx # Presentation engine +├── design-system.jsx # (from brand) +├── styles.css # (from brand) +└── slides/ + ├── manifest.json + └── *.json # Slide content +\`\`\` + +## Step 1: Verify brand exists +\`\`\`bash +ls ${brandPath}/design-system.jsx +\`\`\` +If brand doesn't exist, tell user to run SLIDES_SETUP_BRAND first. + +## Step 2: Create deck directory +\`\`\`bash +mkdir -p ${deckPath}/slides +\`\`\` + +## Step 3: Copy brand files +\`\`\`bash +cp ${brandPath}/design-system.jsx ${deckPath}/ +cp ${brandPath}/styles.css ${deckPath}/ +\`\`\` + +## Step 4: Generate deck files +Call \`DECK_INIT\` with: +- title: "${title}" +- (brandName/brandColor not needed - using existing brand) + +Write to ${deckPath}/: +- index.html +- engine.jsx +- slides/manifest.json + +(Do NOT overwrite design-system.jsx and styles.css - they came from brand) + +## Step 5: Create slides +${ + outline + ? `Create slides based on this outline: +${outline}` + : "Ask the user what slides they need." +} + +Use \`SLIDE_CREATE\` for each slide: +- **title**: Opening slide +- **content**: Main points with bullets +- **stats**: Metrics and KPIs +- **two-column**: Comparisons +- **list**: Feature grids + +Write each slide to ${deckPath}/slides/ +Update manifest.json with slide order. + +## Step 6: Preview +\`\`\`bash +cd ${deckPath} && npx serve +\`\`\` +(Or: \`python -m http.server 8891\`) + +Navigate to http://localhost:8891/ and walk through with user. + +## Finalize +When satisfied: +"✓ Deck saved to ${deckPath}/ +Want me to bundle it into a single portable HTML? (DECK_BUNDLE)"`, + }, + }, + ], + }; + }, + }); + +/** + * SLIDES_ADD_CONTENT - Add slides to an existing deck + */ +export const createAddContentPrompt = (_env: Env) => + createPrompt({ + name: "SLIDES_ADD_CONTENT", + title: "Add Slides to Deck", + description: `Add new slides to an existing presentation. + +Use this when: +- Adding more slides to a deck in progress +- User provides new content to add +- Expanding on existing topics`, + argsSchema: { + deck: z.string().describe("Deck name (e.g., 'q4-results')"), + content: z + .string() + .describe("Content to add (can be notes, bullet points, data, etc.)"), + workspace: z + .string() + .optional() + .describe("Workspace root (default: ~/slides)"), + }, + execute: ({ args }): GetPromptResult => { + const { deck, content } = args; + const workspace = args.workspace || DEFAULT_WORKSPACE; + const deckPath = `${workspace}/decks/${deck}`; + + return { + description: "Add slides with provided content", + messages: [ + { + role: "user", + content: { + type: "text", + text: `# Add Slides to: ${deck} + +## Deck Location +${deckPath}/ + +## Content to Add +${content} + +## Instructions +1. Read ${deckPath}/slides/manifest.json to see existing slides +2. Analyze content and choose layouts: + - Data/numbers → stats + - Comparisons → two-column + - Features/lists → list + - General content → content + +3. Use \`SLIDE_CREATE\` for each new slide +4. Write slide JSON files to ${deckPath}/slides/ +5. Update manifest.json with new slides + +## Preview +Refresh browser to see new slides. Ask if adjustments needed.`, + }, + }, + ], + }; + }, + }); + +/** + * SLIDES_QUICK_START - Fast path for simple presentations + */ +export const createQuickStartPrompt = (_env: Env) => + createPrompt({ + name: "SLIDES_QUICK_START", + title: "Quick Start Presentation", + description: `Create a presentation quickly with minimal setup. + +Use when: +- User wants a quick presentation without brand setup +- Simple one-off presentations +- Demos and prototypes + +Uses a generic brand and creates deck directly. For professional presentations +with custom branding, use SLIDES_SETUP_BRAND instead.`, + argsSchema: { + title: z.string().describe("Presentation title"), + topic: z.string().describe("What the presentation is about"), + logoUrl: z + .string() + .optional() + .describe("Optional: Logo image URL for professional look"), + slideCount: z + .string() + .optional() + .describe("Approximate number of slides (default: 5-7)"), + workspace: z + .string() + .optional() + .describe("Workspace root (default: ~/slides)"), + }, + execute: ({ args }): GetPromptResult => { + const { title, topic, logoUrl } = args; + const workspace = args.workspace || DEFAULT_WORKSPACE; + const count = args.slideCount || "5-7"; + const deckSlug = (title || "presentation") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/-+$/, ""); + const deckPath = `${workspace}/decks/${deckSlug}`; + + return { + description: `Quick presentation: ${title}`, + messages: [ + { + role: "user", + content: { + type: "text", + text: `# Quick Start: ${title} + +## Target +${deckPath}/ + +## Create a ${count} slide presentation about: ${topic} + +### Step 1: Create deck directory +\`\`\`bash +mkdir -p ${deckPath}/slides +\`\`\` + +### Step 2: Initialize with generic brand +Call \`DECK_INIT\` with: +- title: "${title}" +- brandName: "Presenter" +- brandColor: "#3B82F6"${ + logoUrl + ? ` +- assets: { logoUrl: "${logoUrl}" }` + : "" + } + +Write ALL files to ${deckPath}/ +${ + !logoUrl + ? ` +💡 **Tip**: For a more professional look, provide a logoUrl with your logo image.` + : "" +} + +### Step 3: Create slides +Generate ${count} slides covering ${topic}: +1. **title** - Opening: "${title}" +2. **content** - Key points about ${topic} +3. **stats** - (if data available) +4. **content** or **list** - Details +5. **title** or **content** - Closing/summary + +Use \`SLIDE_CREATE\` for each, write to ${deckPath}/slides/ + +### Step 4: Preview +\`\`\`bash +cd ${deckPath} && npx serve +\`\`\` +(Or: \`python -m http.server 8892\`) + +Open http://localhost:8892/ + +### Step 5: Iterate +"Here's your presentation. What would you like to change?"`, + }, + }, + ], + }; + }, + }); + +/** + * SLIDES_LIST - List available brands and decks + */ +export const createListPrompt = (_env: Env) => + createPrompt({ + name: "SLIDES_LIST", + title: "List Brands and Decks", + description: `Show available brands and existing decks in the workspace. + +Use this to: +- See what brands are available +- Find existing decks +- Understand the current workspace state`, + argsSchema: { + workspace: z + .string() + .optional() + .describe("Workspace root (default: ~/slides)"), + }, + execute: ({ args }): GetPromptResult => { + const workspace = args.workspace || DEFAULT_WORKSPACE; + + return { + description: "List available brands and decks", + messages: [ + { + role: "user", + content: { + type: "text", + text: `# List Slides Workspace + +## Workspace +${workspace}/ + +## Check Brands +\`\`\`bash +echo "=== BRANDS ===" && ls -la ${workspace}/brands/ 2>/dev/null || echo "(no brands yet)" +\`\`\` + +## Check Decks +\`\`\`bash +echo "=== DECKS ===" && ls -la ${workspace}/decks/ 2>/dev/null || echo "(no decks yet)" +\`\`\` + +## Summary +For each brand, show: +- Brand name +- Primary color (from styles.css) + +For each deck, show: +- Deck name +- Number of slides (from manifest.json) +- Brand used (if identifiable) + +## Suggest Next Action +If no brands: "Create a brand with SLIDES_SETUP_BRAND" +If brands exist but no decks: "Create a deck with SLIDES_NEW_DECK" +If both exist: "Ready to create presentations!"`, + }, + }, + ], + }; + }, + }); + +/** + * All prompt factory functions. + * Each factory takes env and returns a prompt definition. + */ +export const prompts = [ + createSetupBrandPrompt, + createNewDeckPrompt, + createAddContentPrompt, + createQuickStartPrompt, + createListPrompt, +]; diff --git a/slides/server/resources/index.ts b/slides/server/resources/index.ts new file mode 100644 index 00000000..f85747c2 --- /dev/null +++ b/slides/server/resources/index.ts @@ -0,0 +1,538 @@ +/** + * MCP Apps Resources for Slides MCP + * + * Implements SEP-1865 MCP Apps for displaying slide presentations as UIs. + * + * Resources: + * - ui://slides-viewer - Full presentation viewer with navigation + * - ui://design-system - Brand design system preview + * - ui://slide - Single slide preview + */ +import { createPublicResource } from "@decocms/runtime"; +import type { Env } from "../types/env.ts"; + +/** + * Slide Viewer App - Full presentation viewer with navigation + * + * Receives via ui/initialize: + * - toolInput.slides: Array of slide objects + * - toolInput.title: Presentation title + * - toolInput.brand: Brand configuration + */ +const SLIDES_VIEWER_HTML = ` + + + + + Slides Viewer + + + +
+
+ Presentation + 0 / 0 +
+
+
+
+
+

No slides

+

Waiting for presentation data...

+
+
+
+ +
+
+
+ + +`; + +/** + * Design System Viewer App - Preview brand design system + * + * Receives via ui/initialize: + * - toolInput.brandName: Brand name + * - toolInput.brandColor: Primary brand color + * - toolInput.assets: Logo URLs + */ +const DESIGN_SYSTEM_HTML = ` + + + + + Design System + + + +
+

Design System

+

Brand components for presentations

+
+ +
+

Colors

+
+
+
+ Primary + #8B5CF6 +
+
+
+ +
+

Logo

+
+
+
+ Brand +
+
+
Light Background
+
For content slides
+
+
+
+
+ Brand +
+
+
Dark Background
+
For title slides
+
+
+
+ +
+ +
+

Sample Slides

+
+
+
+
Title Slide
+
+
+
Title Slide
+
Opening slide with brand shapes
+
+
+
+
+
Content Slide
+
+
+
Content Slide
+
Main content with bullets
+
+
+
+
+ + + +`; + +/** + * Single Slide Preview App - Shows one slide + */ +const SINGLE_SLIDE_HTML = ` + + + + + Slide Preview + + + +
+
+
+

Waiting for slide data...

+
+
+
+ + +`; + +/** + * Create resources for MCP Apps + */ +export const createSlidesViewerResource = (_env: Env) => + createPublicResource({ + uri: "ui://slides-viewer", + name: "Slides Viewer", + description: + "Interactive slide presentation viewer with navigation and thumbnails", + mimeType: "text/html;profile=mcp-app", + read: () => ({ + uri: "ui://slides-viewer", + mimeType: "text/html;profile=mcp-app", + text: SLIDES_VIEWER_HTML, + }), + }); + +export const createDesignSystemResource = (_env: Env) => + createPublicResource({ + uri: "ui://design-system", + name: "Design System Preview", + description: + "Brand design system preview showing colors, logos, and components", + mimeType: "text/html;profile=mcp-app", + read: () => ({ + uri: "ui://design-system", + mimeType: "text/html;profile=mcp-app", + text: DESIGN_SYSTEM_HTML, + }), + }); + +export const createSlidePreviewResource = (_env: Env) => + createPublicResource({ + uri: "ui://slide", + name: "Slide Preview", + description: "Single slide preview", + mimeType: "text/html;profile=mcp-app", + read: () => ({ + uri: "ui://slide", + mimeType: "text/html;profile=mcp-app", + text: SINGLE_SLIDE_HTML, + }), + }); + +/** + * All resource factory functions. + * Each factory takes env and returns a resource definition. + */ +export const resources = [ + createSlidesViewerResource, + createDesignSystemResource, + createSlidePreviewResource, +]; diff --git a/slides/server/tools/deck.ts b/slides/server/tools/deck.ts new file mode 100644 index 00000000..509435b4 --- /dev/null +++ b/slides/server/tools/deck.ts @@ -0,0 +1,1642 @@ +/** + * Deck management tools for slide presentations. + * + * These tools handle initialization, info, preview, and bundling of slide decks. + * The system uses JSX with Babel Standalone for browser transpilation. + */ +import { createTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../types/env.ts"; + +// Default style guide template +const DEFAULT_STYLE_TEMPLATE = `# Presentation Style Guide + +This document defines the visual style, tone, and design system for this presentation. +Edit this file to customize the look and feel of your slides. + +## Brand Identity + +**Company/Project:** [Your Company Name] +**Style:** Modern, professional, clean +**Primary Color:** #8B5CF6 (Purple) + +## Color Palette + +\`\`\`css +:root { + /* Primary brand color - used for accents, headings, bullets */ + --brand-primary: #8B5CF6; + --brand-primary-light: #A78BFA; + + /* Background colors */ + --bg-dark: #1a1a1a; + --bg-light: #FFFFFF; + --bg-gray: #E5E5E5; + + /* Text colors */ + --text-dark: #1A1A1A; + --text-light: #FFFFFF; + --text-muted: #6B7280; + + /* Accent for blobs/shapes */ + --shape-dark: #3D3D3D; +} +\`\`\` + +## Typography + +- **Font Family:** Inter, system-ui, sans-serif +- **Title Slides:** 72px, bold, uppercase for impact +- **Content Titles:** 36px, bold, brand color +- **Section Headings:** 24px, semibold, brand color +- **Body Text:** 16px, regular, dark color +- **Tags/Labels:** 12px, uppercase, letter-spacing 0.1em + +## Slide Layouts + +### Title Slide +- Large background shape (blob) with curved edge +- Overlapping accent circle in brand color +- Bold uppercase title +- Logo in bottom-right corner + +### Content Slide +- White/light background +- Logo in top-right header +- Purple/brand colored title and section headings +- Bullet points with brand-colored dots +- Bold key terms in brand color +- Footer with source citations + +### Stats Slide +- Large numbers in brand color +- Labels below each stat +- Grid layout for 3-4 stats + +### Two-Column Slide +- Side-by-side comparison +- Column titles in brand color +- Bullet lists in each column + +### List Slide +- 2x2 grid of items +- Dot + title + description pattern + +## Customizing the Design System + +Edit \`design-system.jsx\` to customize components. Key components: +- \`BrandLogo\`: Your company logo +- \`SlideWrapper\`: Base slide container +- \`TitleSlide\`, \`ContentSlide\`, etc.: Individual layouts + +All styling uses CSS classes from \`styles.css\` with CSS variables for easy theming. +`; + +// Brand assets configuration type +interface BrandAssets { + logoUrl?: string; // Primary logo (usually horizontal, for headers) + logoLightUrl?: string; // Light version for dark backgrounds + logoDarkUrl?: string; // Dark version for light backgrounds + iconUrl?: string; // Square icon (for favicons, small spaces) + brandName: string; // Fallback text if no logo + tagline?: string; // Brand tagline +} + +// Design System JSX - Real JSX syntax +const getDesignSystemJSX = (assets: BrandAssets) => { + const { + brandName, + tagline = "", + logoUrl, + logoLightUrl, + logoDarkUrl, + iconUrl, + } = assets; + + // Determine which logo URLs to embed + const primaryLogo = logoUrl || ""; + const lightLogo = logoLightUrl || primaryLogo; + const darkLogo = logoDarkUrl || primaryLogo; + const icon = iconUrl || primaryLogo; + + return `/** + * Design System + * Brand components for presentations using real JSX + * Requires: React, @babel/standalone + * + * Brand Assets: + * - Primary Logo: ${primaryLogo || "(text fallback)"} + * - Light Logo: ${lightLogo || "(text fallback)"} + * - Dark Logo: ${darkLogo || "(text fallback)"} + * - Icon: ${icon || "(text fallback)"} + */ + +(() => { + // ============================================================================ + // BRAND ASSETS CONFIGURATION + // ============================================================================ + + const BRAND = { + name: "${brandName}", + tagline: "${tagline}", + logos: { + primary: "${primaryLogo}", + light: "${lightLogo}", // For dark backgrounds + dark: "${darkLogo}", // For light backgrounds + icon: "${icon}", // Square icon + }, + hasImageLogo: ${Boolean(primaryLogo)}, + }; + + // ============================================================================ + // BRAND: Logo Component + // ============================================================================ + + function BrandLogo({ size = "normal", variant = "auto", className = "" }) { + const isSmall = size === "small"; + + // Determine which logo to use based on variant + // auto: uses primary, light/dark: uses specific version + const logoSrc = variant === "light" + ? BRAND.logos.light + : variant === "dark" + ? BRAND.logos.dark + : BRAND.logos.primary; + + // If we have an image logo, render it + if (BRAND.hasImageLogo && logoSrc) { + return ( +
+ {BRAND.name} +
+ ); + } + + // Fallback to text logo + return ( +
+ {BRAND.name} + {BRAND.tagline && {BRAND.tagline}} +
+ ); + } + + function BrandIcon({ size = 32, className = "" }) { + if (BRAND.logos.icon) { + return ( + {BRAND.name} + ); + } + + // Fallback: first letter of brand name + return ( +
+ {BRAND.name.charAt(0).toUpperCase()} +
+ ); + } + + // ============================================================================ + // LAYOUT: Slide Wrapper + // ============================================================================ + + function SlideWrapper({ children, variant = "content", className = "" }) { + return ( +
+ {children} +
+ ); + } + + function SlideHeader() { + return ( +
+ +
+ ); + } + + function SlideFooter({ source, label }) { + if (!source) return null; + + return ( +
+ Source: {source} + {label && {label}} +
+
+ ); + } + + function Tag({ children }) { + if (!children) return null; + return {children}; + } + + // ============================================================================ + // CONTENT: Bullets + // ============================================================================ + + function BulletList({ items, nested = false }) { + if (!items?.length) return null; + + return ( +
    + {items.map((item, idx) => ( +
  • + {item.highlight ? ( + {item.text} + ) : ( + item.text + )} +
  • + ))} +
+ ); + } + + function Section({ title, bullets, nestedBullets }) { + return ( +
+ {title &&

{title}

} + + +
+ ); + } + + // ============================================================================ + // SLIDES: Title + // ============================================================================ + + function TitleSlide({ slide }) { + return ( + +
+
+
+

{slide.title}

+
+
+ +
+ + ); + } + + // ============================================================================ + // SLIDES: Content + // ============================================================================ + + function ContentSlide({ slide }) { + const items = slide.items || []; + + return ( + + +
+

{slide.title}

+ {items.map((item, idx) => ( +
+ ))} +
+ +
+ ); + } + + // ============================================================================ + // SLIDES: Stats + // ============================================================================ + + function StatItem({ value, label }) { + return ( +
+
{value}
+
{label}
+
+ ); + } + + function StatsSlide({ slide }) { + const items = slide.items || []; + + return ( + + + {slide.tag} +

{slide.title}

+
+ {items.map((item, idx) => ( + + ))} +
+
+ ); + } + + // ============================================================================ + // SLIDES: Two Column + // ============================================================================ + + function Column({ title, bullets }) { + return ( +
+ {title &&

{title}

} + +
+ ); + } + + function TwoColumnSlide({ slide }) { + const [left, right] = slide.items || []; + + return ( + + +
+ {slide.tag} +

{slide.title}

+
+ + +
+
+
+ ); + } + + // ============================================================================ + // SLIDES: List (2x2 Grid) + // ============================================================================ + + function ListItem({ title, subtitle }) { + return ( +
+
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+ ); + } + + function ListSlide({ slide }) { + const items = slide.items || []; + + return ( + + +
+ {slide.tag} +

{slide.title}

+ {slide.subtitle &&

{slide.subtitle}

} +
+ {items.map((item, idx) => ( + + ))} +
+
+
+ ); + } + + // ============================================================================ + // COMPONENT REGISTRY - Expose globally + // ============================================================================ + + window.DesignSystem = { + // Brand configuration + BRAND, + // Slide components + SlideComponents: { + title: TitleSlide, + content: ContentSlide, + stats: StatsSlide, + "two-column": TwoColumnSlide, + list: ListSlide, + }, + // Individual components for custom slides + BrandLogo, + BrandIcon, + TitleSlide, + ContentSlide, + StatsSlide, + TwoColumnSlide, + ListSlide, + // Building blocks + SlideWrapper, + SlideHeader, + SlideFooter, + BulletList, + Section, + Tag, + }; + + console.log("✓ Design System loaded"); + console.log(" Brand:", BRAND.name); + console.log(" Has image logo:", BRAND.hasImageLogo); +})(); +`; +}; + +// Engine JSX - Real JSX syntax +const getEngineJSX = () => `/** + * Presentation Engine + * Core logic for slide navigation, scaling, and rendering + * Requires: React, ReactDOM, design-system.jsx loaded first + */ + +(() => { + const { useState, useEffect, useRef } = React; + + // Base dimensions (16:9 aspect ratio) + const BASE_WIDTH = 1366; + const BASE_HEIGHT = 768; + + // ============================================================================ + // NAVIGATION + // ============================================================================ + + function Navigation({ current, total, onNavigate, disabled }) { + const goFirst = () => onNavigate(0); + const goPrev = () => onNavigate(current - 1); + const goNext = () => onNavigate(current + 1); + + const toggleFullscreen = () => { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else { + document.documentElement.requestFullscreen(); + } + }; + + const indicator = \`\${String(current + 1).padStart(2, "0")} / \${String(total).padStart(2, "0")}\`; + + return ( +
+ {indicator} + + + + +
+ ); + } + + // ============================================================================ + // PRESENTATION + // ============================================================================ + + function Presentation({ slides, title, subtitle }) { + const [currentSlide, setCurrentSlide] = useState(0); + const [isAnimating, setIsAnimating] = useState(false); + const [scale, setScale] = useState(1); + const containerRef = useRef(null); + + const { SlideComponents } = window.DesignSystem; + + // Calculate scale to fit viewport + useEffect(() => { + const calculateScale = () => { + if (!containerRef.current) return; + const { clientWidth, clientHeight } = containerRef.current; + const scaleX = clientWidth / BASE_WIDTH; + const scaleY = clientHeight / BASE_HEIGHT; + setScale(Math.min(scaleX, scaleY) * 0.95); + }; + + calculateScale(); + window.addEventListener("resize", calculateScale); + return () => window.removeEventListener("resize", calculateScale); + }, []); + + // Keyboard navigation + useEffect(() => { + const handleKeyDown = (e) => { + if (isAnimating) return; + + const actions = { + ArrowRight: () => goToSlide(Math.min(currentSlide + 1, slides.length - 1)), + ArrowDown: () => goToSlide(Math.min(currentSlide + 1, slides.length - 1)), + " ": () => goToSlide(Math.min(currentSlide + 1, slides.length - 1)), + ArrowLeft: () => goToSlide(Math.max(currentSlide - 1, 0)), + ArrowUp: () => goToSlide(Math.max(currentSlide - 1, 0)), + Home: () => goToSlide(0), + End: () => goToSlide(slides.length - 1), + }; + + if (actions[e.key]) { + e.preventDefault(); + actions[e.key](); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [currentSlide, isAnimating, slides.length]); + + const goToSlide = (index) => { + if (index === currentSlide || isAnimating) return; + setIsAnimating(true); + setCurrentSlide(index); + setTimeout(() => setIsAnimating(false), 300); + }; + + // Render current slide + const slide = slides[currentSlide]; + const SlideComponent = SlideComponents[slide?.layout] || SlideComponents.content; + + const displayWidth = BASE_WIDTH * scale; + const displayHeight = BASE_HEIGHT * scale; + + return ( +
+
+
+ {slide && } +
+
+ + +
+ ); + } + + // ============================================================================ + // INITIALIZATION + // ============================================================================ + + async function initPresentation(manifestPath = "./slides/manifest.json") { + try { + const response = await fetch(manifestPath); + const manifest = await response.json(); + + const slides = await Promise.all( + manifest.slides.map(async (slideInfo) => { + try { + const slideResponse = await fetch(\`./slides/\${slideInfo.file}\`); + const slideData = await slideResponse.json(); + return { ...slideInfo, ...slideData }; + } catch (e) { + console.error(\`Failed to load slide: \${slideInfo.file}\`, e); + return slideInfo; + } + }) + ); + + const root = ReactDOM.createRoot(document.getElementById("root")); + root.render( + + ); + + console.log(\`✓ Presentation loaded: \${slides.length} slides\`); + } catch (e) { + console.error("Failed to initialize presentation:", e); + document.getElementById("root").innerHTML = \` +
+

Presentation not found

+

Create slides/manifest.json to get started.

+
+ \`; + } + } + + window.Presentation = Presentation; + window.initPresentation = initPresentation; + + console.log("✓ Engine loaded"); +})(); +`; + +// Design System Viewer HTML +const getDesignViewerHTML = (brandColor = "#8B5CF6") => ` + + + + + Design System Viewer + + + + + + + +
+ + + +`; + +// CSS with variables for easy customization +const getStylesCSS = () => `/* Slides Presentation Styles */ +/* Customize by editing the CSS variables below */ + +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + +:root { + /* Brand Colors - CUSTOMIZE THESE */ + --brand-primary: #8B5CF6; + --brand-primary-light: #A78BFA; + + /* Background Colors */ + --bg-shape-dark: #3D3D3D; + --bg-gray: #E5E5E5; + --bg-white: #FFFFFF; + --bg-dark: #1a1a1a; + + /* Text Colors */ + --text-dark: #1A1A1A; + --text-light: #FFFFFF; + --text-muted: #6B7280; + --text-secondary: #9CA3AF; + + /* Slide Dimensions */ + --slide-width: 1366px; + --slide-height: 768px; + --margin-x: 48px; + --margin-y: 40px; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } +html, body { + width: 100%; height: 100%; overflow: hidden; + font-family: 'Inter', system-ui, sans-serif; + background: var(--bg-dark); + -webkit-font-smoothing: antialiased; +} +#root { width: 100%; height: 100%; } + +.presentation-container { + width: 100vw; height: 100vh; + display: flex; align-items: center; justify-content: center; + background: var(--bg-dark); +} + +.slide { + width: var(--slide-width); height: var(--slide-height); + position: absolute; overflow: hidden; + transform-origin: center center; +} + +/* Title Slide */ +.slide--title { background: var(--bg-gray); } +.blob-primary { + position: absolute; width: 70%; height: 130%; + background: var(--bg-shape-dark); + border-radius: 0 50% 50% 0; left: 0; top: -15%; z-index: 1; +} +.blob-accent { + position: absolute; width: 380px; height: 380px; + background: var(--brand-primary); + border-radius: 50%; right: 12%; top: -8%; z-index: 2; +} +.slide--title .slide-content { + position: relative; z-index: 3; + padding: 0 var(--margin-x); height: 100%; + display: flex; flex-direction: column; justify-content: center; +} +.title-hero { + font-size: 72px; font-weight: 700; color: var(--text-light); + line-height: 1.1; letter-spacing: -0.02em; + text-transform: uppercase; max-width: 60%; +} +.logo-container { + position: absolute; bottom: var(--margin-y); + right: var(--margin-x); z-index: 3; +} + +/* Brand Logo - Image version */ +.logo-brand { display: flex; flex-direction: column; } +.logo-brand--image { align-items: flex-start; } +.logo-brand--image .logo-brand-image { + height: 48px; width: auto; max-width: 200px; + object-fit: contain; object-position: left center; +} +.logo-brand--image.logo-small .logo-brand-image { + height: 32px; max-width: 140px; +} + +/* Brand Logo - Text fallback */ +.logo-brand--text { font-size: 42px; } +.logo-brand--text.logo-small { font-size: 28px; } +.logo-brand-wordmark { + font-weight: 700; color: var(--bg-shape-dark); letter-spacing: -0.02em; +} +.logo-brand-tagline { + font-size: 11px; font-weight: 500; color: var(--bg-shape-dark); + letter-spacing: 0.2em; text-transform: uppercase; margin-top: 2px; +} + +/* Brand Icon */ +.brand-icon { display: flex; align-items: center; justify-content: center; } +.brand-icon--text { + background: var(--brand-primary); color: white; font-weight: 700; + border-radius: 8px; +} + +/* Content Slide */ +.slide--content { + background: var(--bg-white); + padding: var(--margin-y) var(--margin-x); + display: flex; flex-direction: column; +} +.slide-header { + display: flex; justify-content: flex-end; margin-bottom: 16px; +} +.slide-body { flex: 1; overflow: hidden; } +.slide-title { + font-size: 36px; font-weight: 700; + color: var(--brand-primary); line-height: 1.2; margin-bottom: 20px; +} +.slide-tag { + font-size: 12px; font-weight: 500; color: var(--text-secondary); + letter-spacing: 0.1em; text-transform: uppercase; + margin-bottom: 8px; display: block; +} +.slide-subtitle { + font-size: 16px; color: var(--text-muted); margin-bottom: 16px; +} +.section-heading { + font-size: 24px; font-weight: 600; + color: var(--brand-primary); line-height: 1.3; + margin-top: 28px; margin-bottom: 16px; +} + +/* Bullet Lists */ +.bullet-list { list-style: none; padding: 0; margin: 0 0 16px 0; } +.bullet-list > li { + position: relative; padding-left: 24px; margin-bottom: 14px; + font-size: 16px; line-height: 1.5; color: var(--text-dark); +} +.bullet-list > li::before { + content: ''; position: absolute; left: 0; top: 8px; + width: 8px; height: 8px; background: var(--brand-primary); border-radius: 50%; +} +.bullet-list--nested { margin-left: 32px; margin-top: 10px; } +.bullet-list--nested > li::before { + background: transparent; border: 2px solid var(--brand-primary); + width: 6px; height: 6px; +} +.text-bold { font-weight: 600; color: var(--brand-primary); } + +/* Footer */ +.slide-footer { + display: flex; align-items: center; gap: 16px; + padding-top: 16px; margin-top: auto; position: relative; +} +.footer-text { font-size: 11px; color: var(--text-muted); } +.footer-label { font-size: 11px; font-weight: 500; color: var(--text-muted); } +.footer-dot { + position: absolute; right: 0; bottom: 0; + width: 40px; height: 40px; background: var(--brand-primary-light); + border-radius: 50%; opacity: 0.7; +} + +/* Stats Slide */ +.stats-grid { + display: grid; grid-template-columns: repeat(4, 1fr); + gap: 32px; margin-top: auto; margin-bottom: auto; +} +.stat-item { text-align: center; } +.stat-value { + font-size: 64px; font-weight: 700; + color: var(--brand-primary); line-height: 1; margin-bottom: 8px; +} +.stat-label { font-size: 16px; font-weight: 500; color: var(--text-muted); } + +/* Two Column */ +.columns { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; margin-top: 24px; } +.column-title { + font-size: 20px; font-weight: 600; + color: var(--brand-primary); margin-bottom: 16px; +} + +/* List Slide */ +.list-grid { + display: grid; grid-template-columns: repeat(2, 1fr); + gap: 32px 48px; margin-top: 32px; +} +.list-item { display: flex; align-items: flex-start; gap: 16px; } +.list-item-dot { + width: 10px; height: 10px; background: var(--brand-primary); + border-radius: 50%; flex-shrink: 0; margin-top: 6px; +} +.list-item-content h4 { + font-size: 18px; font-weight: 600; color: var(--text-dark); margin-bottom: 4px; +} +.list-item-content p { font-size: 14px; color: var(--text-muted); line-height: 1.4; } + +/* Navigation */ +.nav-controls { + position: fixed; bottom: 24px; right: 24px; + display: flex; align-items: center; gap: 12px; z-index: 100; +} +.nav-indicator { + font-size: 13px; color: rgba(255,255,255,0.5); + font-variant-numeric: tabular-nums; margin-right: 8px; +} +.nav-btn { + width: 40px; height: 40px; border-radius: 50%; + display: flex; align-items: center; justify-content: center; + background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); + color: rgba(255,255,255,0.8); cursor: pointer; transition: all 0.2s; +} +.nav-btn:hover:not(:disabled) { background: rgba(255,255,255,0.2); } +.nav-btn:disabled { opacity: 0.3; cursor: not-allowed; } +.nav-btn--primary { + background: var(--brand-primary); border-color: var(--brand-primary); color: white; +} + +/* Error & Loading states */ +.error { + display: flex; flex-direction: column; align-items: center; justify-content: center; + height: 100vh; color: var(--brand-primary); font-family: Inter, sans-serif; +} +.error h1 { font-size: 36px; font-weight: 700; } +.error p { font-size: 16px; color: var(--text-muted); margin-top: 8px; } +.loading { + display: flex; align-items: center; justify-content: center; + height: 100vh; color: var(--text-muted); font-family: Inter, sans-serif; +} +`; + +// Schema for brand assets +const BrandAssetsSchema = z.object({ + logoUrl: z + .string() + .optional() + .describe( + "Primary logo image URL (horizontal format, for headers). Required for professional brands.", + ), + logoLightUrl: z + .string() + .optional() + .describe( + "Light version of logo for dark backgrounds. Falls back to logoUrl if not provided.", + ), + logoDarkUrl: z + .string() + .optional() + .describe( + "Dark version of logo for light backgrounds. Falls back to logoUrl if not provided.", + ), + iconUrl: z + .string() + .optional() + .describe( + "Square icon/favicon (for small spaces, browser tabs). Falls back to logoUrl if not provided.", + ), +}); + +/** + * DECK_INIT - Initialize a new slide deck with JSX architecture + */ +export const createDeckInitTool = (_env: Env) => + createTool({ + id: "DECK_INIT", + _meta: { "ui/resourceUri": "ui://design-system" }, + description: `Initialize a new slide deck or brand. + +**Two modes:** + +1. **Create a BRAND** (no 'brand' parameter): + Creates all files for a reusable brand in brands/{brandSlug}/ + - design-system.jsx, styles.css, design.html, style.md + + **IMPORTANT: Brand Assets** + For professional brands, you should provide image URLs for logos: + - \`logoUrl\`: Primary horizontal logo (required for real brands) + - \`logoLightUrl\`: Light version for dark backgrounds (optional) + - \`logoDarkUrl\`: Dark version for light backgrounds (optional) + - \`iconUrl\`: Square icon for favicons/small spaces (optional) + + If no logoUrl is provided, falls back to text-based logo using brandName. + +2. **Create a DECK** (with 'brand' parameter): + Creates only deck files in decks/{deckSlug}/, references brand + - index.html (loads from ../../brands/{brand}/) + - engine.jsx + - slides/manifest.json + +The deck's index.html will load design-system.jsx and styles.css from the brand folder.`, + inputSchema: z.object({ + title: z.string().describe("Presentation or brand title"), + subtitle: z.string().optional().describe("Presentation subtitle or date"), + brand: z + .string() + .optional() + .describe( + "Brand slug to use (e.g., 'cogna'). If provided, creates a deck referencing this brand. If not provided, creates a new brand.", + ), + brandName: z + .string() + .optional() + .describe("Brand name (used for alt text and text fallback)"), + brandTagline: z + .string() + .optional() + .describe("Brand tagline (only for new brands)"), + brandColor: z + .string() + .optional() + .describe("Primary brand color in hex (only for new brands)"), + assets: BrandAssetsSchema.optional().describe( + "Brand image assets (logos, icons). Provide these for professional-looking presentations.", + ), + }), + outputSchema: z.object({ + files: z + .array( + z.object({ + path: z.string().describe("Relative file path"), + content: z.string().describe("File content"), + }), + ) + .describe("Files to create"), + message: z.string(), + mode: z + .enum(["brand", "deck"]) + .describe("Whether this created a brand or deck"), + }), + execute: async ({ context }) => { + const { + title, + subtitle, + brand, + brandName, + brandTagline, + brandColor, + assets, + } = context; + const now = new Date().toISOString(); + + // MODE: Create a DECK referencing an existing brand + if (brand) { + const brandPath = `../../brands/${brand}`; + + const manifest = { + title: title || "Presentation", + subtitle: subtitle || "", + brand: brand, + createdAt: now, + updatedAt: now, + slides: [], + }; + + const deckIndexHtml = ` + + + + + ${title || "Presentation"} + + + + + + + + + + + + + + + +
Loading...
+ + + + + + + + + + +`; + + return { + files: [ + { path: "index.html", content: deckIndexHtml }, + { path: "engine.jsx", content: getEngineJSX() }, + { + path: "slides/manifest.json", + content: JSON.stringify(manifest, null, 2), + }, + ], + message: `Deck "${title}" created using brand "${brand}". Save to decks/{deck-name}/. Serve with: npx serve`, + mode: "deck" as const, + }; + } + + // MODE: Create a new BRAND + let stylesCSS = getStylesCSS(); + if (brandColor) { + stylesCSS = stylesCSS.replace(/#8B5CF6/g, brandColor); + } + + // Build brand assets object + const brandAssets: BrandAssets = { + brandName: brandName || title || "Brand", + tagline: brandTagline, + logoUrl: assets?.logoUrl, + logoLightUrl: assets?.logoLightUrl, + logoDarkUrl: assets?.logoDarkUrl, + iconUrl: assets?.iconUrl, + }; + + const hasImageAssets = Boolean(assets?.logoUrl); + + return { + files: [ + { + path: "design-system.jsx", + content: getDesignSystemJSX(brandAssets), + }, + { path: "styles.css", content: stylesCSS }, + { + path: "design.html", + content: getDesignViewerHTML(brandColor || "#8B5CF6"), + }, + { path: "style.md", content: DEFAULT_STYLE_TEMPLATE }, + { + path: "brand-assets.json", + content: JSON.stringify( + { + name: brandAssets.brandName, + tagline: brandAssets.tagline || "", + color: brandColor || "#8B5CF6", + assets: { + logo: assets?.logoUrl || null, + logoLight: assets?.logoLightUrl || null, + logoDark: assets?.logoDarkUrl || null, + icon: assets?.iconUrl || null, + }, + hasImageAssets, + createdAt: now, + }, + null, + 2, + ), + }, + ], + message: hasImageAssets + ? `Brand "${brandAssets.brandName}" created with image logo. Save to brands/{brand-slug}/. Preview design system at /design.html` + : `Brand "${brandAssets.brandName}" created with text fallback logo. For professional presentations, provide logoUrl with actual brand images. Save to brands/{brand-slug}/. Preview design system at /design.html`, + mode: "brand" as const, + }; + }, + }); + +/** + * DECK_INFO - Get information about a slide deck + */ +export const createDeckInfoTool = (_env: Env) => + createTool({ + id: "DECK_INFO", + description: "Get information about a slide deck from its manifest.", + inputSchema: z.object({ + manifest: z.string().describe("Content of manifest.json"), + }), + outputSchema: z.object({ + title: z.string(), + subtitle: z.string(), + slideCount: z.number(), + slides: z.array( + z.object({ id: z.string(), title: z.string(), layout: z.string() }), + ), + }), + execute: async ({ context }) => { + const manifest = JSON.parse(context.manifest); + return { + title: manifest.title || "Untitled", + subtitle: manifest.subtitle || "", + slideCount: manifest.slides?.length || 0, + slides: (manifest.slides || []).map((s: any) => ({ + id: s.id || s.file, + title: s.title || "Untitled", + layout: s.layout || "content", + })), + }; + }, + }); + +/** + * DECK_BUNDLE - Bundle all slides into a single HTML file + */ +export const createDeckBundleTool = (_env: Env) => + createTool({ + id: "DECK_BUNDLE", + description: + "Bundle the entire slide deck into a single portable HTML file with embedded JSX.", + inputSchema: z.object({ + manifest: z.string().describe("Content of manifest.json"), + slides: z.array( + z.object({ + file: z.string(), + content: z.string().describe("JSON content of slide file"), + }), + ), + stylesCss: z.string().describe("Content of styles.css"), + designSystemJsx: z.string().describe("Content of design-system.jsx"), + }), + outputSchema: z.object({ + html: z.string().describe("Complete bundled HTML file"), + slideCount: z.number(), + }), + execute: async ({ context }) => { + const manifest = JSON.parse(context.manifest); + const slides = context.slides.map((s) => JSON.parse(s.content)); + + const bundledHtml = ` + + + + + ${manifest.title || "Presentation"} + + + + + + +
+ + + + +`; + + return { html: bundledHtml, slideCount: slides.length }; + }, + }); + +/** + * DECK_GET_ENGINE - Get the presentation engine JSX + */ +export const createDeckGetEngineTool = (_env: Env) => + createTool({ + id: "DECK_GET_ENGINE", + description: + "Get the presentation engine JSX. Save as engine.jsx in the deck directory.", + inputSchema: z.object({}), + outputSchema: z.object({ + content: z.string().describe("JSX engine content"), + }), + execute: async () => ({ content: getEngineJSX() }), + }); + +/** + * DECK_GET_DESIGN_SYSTEM - Get the design system JSX + */ +export const createDeckGetDesignSystemTool = (_env: Env) => + createTool({ + id: "DECK_GET_DESIGN_SYSTEM", + description: + "Get the design system JSX template. Customize brand name/tagline and save as design-system.jsx.", + inputSchema: z.object({ + brandName: z.string().optional().describe("Brand name for logo"), + brandTagline: z.string().optional().describe("Brand tagline"), + }), + outputSchema: z.object({ + content: z.string().describe("JSX design system content"), + }), + execute: async ({ context }) => ({ + content: getDesignSystemJSX({ + brandName: context.brandName || "Brand", + tagline: context.brandTagline || "TAGLINE", + }), + }), + }); + +/** + * BRAND_ASSETS_VALIDATE - Validate and prepare brand assets + */ +export const createBrandAssetsValidateTool = (_env: Env) => + createTool({ + id: "BRAND_ASSETS_VALIDATE", + description: `Validate brand assets and get recommendations for what's missing. + +Use this tool to: +1. Check if provided asset URLs are valid +2. Get a list of required vs optional assets +3. Get suggestions for finding missing assets + +This should be called BEFORE DECK_INIT when setting up a new brand.`, + inputSchema: z.object({ + brandName: z.string().describe("Brand/company name"), + brandWebsite: z + .string() + .optional() + .describe("Brand website URL (for asset discovery suggestions)"), + assets: z + .object({ + logoUrl: z.string().optional().describe("Primary logo URL"), + logoLightUrl: z.string().optional().describe("Light logo URL"), + logoDarkUrl: z.string().optional().describe("Dark logo URL"), + iconUrl: z.string().optional().describe("Icon/favicon URL"), + }) + .optional() + .describe("Currently provided assets"), + }), + outputSchema: z.object({ + isComplete: z + .boolean() + .describe("Whether minimum required assets are provided"), + providedAssets: z + .array(z.string()) + .describe("List of assets that were provided"), + missingRequired: z + .array(z.string()) + .describe("List of required assets that are missing"), + missingOptional: z + .array(z.string()) + .describe("List of optional assets that could improve the brand"), + suggestions: z + .array(z.string()) + .describe("Suggestions for finding missing assets"), + assetRequirements: z.object({ + logo: z.object({ + description: z.string(), + required: z.boolean(), + recommendedFormat: z.string(), + recommendedSize: z.string(), + }), + logoLight: z.object({ + description: z.string(), + required: z.boolean(), + recommendedFormat: z.string(), + recommendedSize: z.string(), + }), + logoDark: z.object({ + description: z.string(), + required: z.boolean(), + recommendedFormat: z.string(), + recommendedSize: z.string(), + }), + icon: z.object({ + description: z.string(), + required: z.boolean(), + recommendedFormat: z.string(), + recommendedSize: z.string(), + }), + }), + }), + execute: async ({ context }) => { + const { brandName, brandWebsite, assets } = context; + + const providedAssets: string[] = []; + const missingRequired: string[] = []; + const missingOptional: string[] = []; + const suggestions: string[] = []; + + // Check provided assets + if (assets?.logoUrl) { + providedAssets.push("Primary Logo (logoUrl)"); + } else { + missingRequired.push("Primary Logo (logoUrl)"); + } + + if (assets?.logoLightUrl) { + providedAssets.push("Light Logo (logoLightUrl)"); + } else { + missingOptional.push( + "Light Logo (logoLightUrl) - for dark backgrounds", + ); + } + + if (assets?.logoDarkUrl) { + providedAssets.push("Dark Logo (logoDarkUrl)"); + } else { + missingOptional.push("Dark Logo (logoDarkUrl) - for light backgrounds"); + } + + if (assets?.iconUrl) { + providedAssets.push("Icon (iconUrl)"); + } else { + missingOptional.push("Icon (iconUrl) - for favicon and small spaces"); + } + + // Generate suggestions based on what's missing + if (missingRequired.length > 0) { + suggestions.push( + `Ask the user: "Please provide a URL or file path for your ${brandName} logo"`, + ); + + if (brandWebsite) { + suggestions.push( + `Try checking ${brandWebsite}/logo.png or ${brandWebsite}/logo.svg`, + ); + suggestions.push( + `Look for the logo in the website's header or footer`, + ); + suggestions.push( + `Check the tag in the page source`, + ); + suggestions.push(`Look for ${brandWebsite}/favicon.ico for the icon`); + } + + suggestions.push( + `If the user has a brand guidelines PDF, ask them to share it`, + ); + suggestions.push( + `The user can upload images to a service like imgur.com and share the URL`, + ); + } + + return { + isComplete: missingRequired.length === 0, + providedAssets, + missingRequired, + missingOptional, + suggestions, + assetRequirements: { + logo: { + description: + "Primary horizontal logo, displayed in slide headers and title slides", + required: true, + recommendedFormat: "PNG with transparent background, or SVG", + recommendedSize: "At least 400px wide, height 80-120px", + }, + logoLight: { + description: "Light/white version of the logo for dark backgrounds", + required: false, + recommendedFormat: "PNG with transparent background, or SVG", + recommendedSize: "Same as primary logo", + }, + logoDark: { + description: "Dark/black version of the logo for light backgrounds", + required: false, + recommendedFormat: "PNG with transparent background, or SVG", + recommendedSize: "Same as primary logo", + }, + icon: { + description: "Square icon for favicon and small display areas", + required: false, + recommendedFormat: "PNG or SVG", + recommendedSize: "64x64px or larger, square (1:1 ratio)", + }, + }, + }; + }, + }); + +export const deckTools = [ + createDeckInitTool, + createDeckInfoTool, + createDeckBundleTool, + createDeckGetEngineTool, + createDeckGetDesignSystemTool, + createBrandAssetsValidateTool, +]; diff --git a/slides/server/tools/index.ts b/slides/server/tools/index.ts new file mode 100644 index 00000000..9d4e1f4c --- /dev/null +++ b/slides/server/tools/index.ts @@ -0,0 +1,26 @@ +/** + * Central export point for all slides MCP tools. + * + * This file aggregates all tools from different domains: + * - Deck tools: initialization, info, bundling, engine + * - Style tools: style guide management + * - Slide tools: CRUD operations for slides + * + * Note: Brand research is handled by the separate Brand MCP. + * Configure the BRAND binding to use brand discovery features. + */ +import { deckTools } from "./deck.ts"; +import { styleTools } from "./style.ts"; +import { slideTools } from "./slides.ts"; + +/** + * All tool factory functions. + * Each factory takes env and returns a tool definition. + * The runtime will call these with the environment. + */ +export const tools = [...deckTools, ...styleTools, ...slideTools]; + +// Re-export individual tool modules for direct access +export { deckTools } from "./deck.ts"; +export { styleTools } from "./style.ts"; +export { slideTools } from "./slides.ts"; diff --git a/slides/server/tools/slides.ts b/slides/server/tools/slides.ts new file mode 100644 index 00000000..a6635fad --- /dev/null +++ b/slides/server/tools/slides.ts @@ -0,0 +1,597 @@ +/** + * Slide management tools for slide presentations. + * + * These tools handle CRUD operations for individual slides including + * creation, updates, deletion, listing, and reordering. + * + * SLIDE LAYOUTS: + * - title: Opening slide with large title, decorative shapes, and logo + * - content: Main content slide with title, sections, bullets, and footer + * - stats: Large numbers in a grid (3-4 stats) + * - two-column: Side-by-side comparison with column titles and bullets + * - list: 2x2 grid of items with title + description + * - quote: Centered quote with attribution + * - image: Full background image with overlay text + * - custom: Raw HTML content + */ +import { createTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../types/env.ts"; + +// Shared schemas +const LayoutSchema = z.enum([ + "title", + "content", + "two-column", + "stats", + "list", + "quote", + "image", + "custom", +]); + +const BulletSchema = z.object({ + text: z.string().describe("Bullet point text"), + highlight: z + .boolean() + .optional() + .describe("Highlight in brand color (for key terms)"), +}); + +const SlideItemSchema = z.object({ + title: z.string().optional().describe("Section title or heading"), + subtitle: z.string().optional().describe("Description or subtext"), + value: z + .string() + .optional() + .describe("For stats: the number (e.g., '2,847', '89%', 'R$42M')"), + label: z.string().optional().describe("For stats: label below the number"), + bullets: z + .array(BulletSchema) + .optional() + .describe("Bullet points with optional highlighting"), + nestedBullets: z + .array(BulletSchema) + .optional() + .describe("Second-level bullets (hollow circles)"), +}); + +const SlideDataSchema = z.object({ + id: z.string().describe("Unique slide identifier"), + layout: LayoutSchema.describe("Slide layout type"), + title: z.string().describe("Main slide title"), + subtitle: z.string().optional().describe("Subtitle or description"), + tag: z + .string() + .optional() + .describe("Small uppercase label above title (e.g., 'METHODOLOGY')"), + items: z + .array(SlideItemSchema) + .optional() + .describe("Content items (sections, stats, list items)"), + source: z + .string() + .optional() + .describe( + "Source citation for footer (e.g., 'Company Report - July 2023')", + ), + label: z + .string() + .optional() + .describe("Footer label (e.g., 'Public', 'Confidential')"), + backgroundImage: z + .string() + .optional() + .describe("URL for background image (image layout)"), + customHtml: z + .string() + .optional() + .describe("Raw HTML content (custom layout only)"), +}); + +/** + * SLIDE_CREATE - Create a new slide + */ +export const createSlideCreateTool = (_env: Env) => + createTool({ + id: "SLIDE_CREATE", + _meta: { "ui/resourceUri": "ui://slide" }, + description: `Create a new slide. Returns slide JSON to save and updated manifest. + +LAYOUT EXAMPLES: + +**title** - Opening slide + { layout: "title", title: "PRESENTATION TITLE" } + +**content** - Main content with bullets + { layout: "content", title: "What is X?", tag: "OVERVIEW", + items: [{ title: "Key Points", bullets: [{ text: "First point" }, { text: "Important", highlight: true }] }], + source: "Company Report 2023", label: "Public" } + +**stats** - Large numbers + { layout: "stats", title: "Key Metrics", tag: "RESULTS", + items: [{ value: "2,847", label: "Total Users" }, { value: "89%", label: "Success Rate" }] } + +**two-column** - Side-by-side + { layout: "two-column", title: "Comparison", tag: "ANALYSIS", + items: [{ title: "Option A", bullets: [...] }, { title: "Option B", bullets: [...] }] } + +**list** - 2x2 grid of items + { layout: "list", title: "Our Services", subtitle: "What we offer", + items: [{ title: "Service 1", subtitle: "Description" }, ...] }`, + inputSchema: z.object({ + manifest: z.string().describe("Current manifest.json content"), + layout: LayoutSchema.describe("Slide layout type"), + title: z.string().describe("Main slide title"), + subtitle: z.string().optional().describe("Subtitle or description"), + tag: z.string().optional().describe("Uppercase label above title"), + items: z.array(SlideItemSchema).optional().describe("Content items"), + source: z.string().optional().describe("Footer source citation"), + label: z.string().optional().describe("Footer label"), + backgroundImage: z.string().optional().describe("Background image URL"), + customHtml: z.string().optional().describe("Raw HTML (custom layout)"), + position: z.number().optional().describe("Insert position (0-indexed)"), + }), + outputSchema: z.object({ + slideFile: z.object({ + filename: z + .string() + .describe("Filename for the slide (e.g., '001-title.json')"), + content: z.string().describe("JSON content to write to the slide file"), + }), + updatedManifest: z.string().describe("Updated manifest.json content"), + slideId: z.string().describe("ID of the created slide"), + position: z.number().describe("Position of the slide in the deck"), + }), + execute: async ({ context }) => { + const { manifest: manifestStr, position, ...slideData } = context; + const manifest = JSON.parse(manifestStr); + + // Generate slide ID and filename + const slideIndex = manifest.slides?.length || 0; + const slideNum = String(slideIndex + 1).padStart(3, "0"); + const slideId = `slide-${slideNum}-${Date.now()}`; + const slugTitle = slideData.title + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, "") + .substring(0, 20); + const filename = `${slideNum}-${slugTitle || slideData.layout}.json`; + + // Create slide object (only include defined fields) + const slide: Record = { + id: slideId, + layout: slideData.layout, + title: slideData.title, + }; + + if (slideData.subtitle) slide.subtitle = slideData.subtitle; + if (slideData.tag) slide.tag = slideData.tag; + if (slideData.items?.length) slide.items = slideData.items; + if (slideData.source) slide.source = slideData.source; + if (slideData.label) slide.label = slideData.label; + if (slideData.backgroundImage) + slide.backgroundImage = slideData.backgroundImage; + if (slideData.customHtml) slide.customHtml = slideData.customHtml; + + // Update manifest + const manifestEntry = { + id: slideId, + file: filename, + title: slideData.title, + layout: slideData.layout, + }; + + if (!manifest.slides) { + manifest.slides = []; + } + + const insertPosition = + position !== undefined && + position >= 0 && + position <= manifest.slides.length + ? position + : manifest.slides.length; + + manifest.slides.splice(insertPosition, 0, manifestEntry); + manifest.updatedAt = new Date().toISOString(); + + return { + slideFile: { + filename, + content: JSON.stringify(slide, null, 2), + }, + updatedManifest: JSON.stringify(manifest, null, 2), + slideId, + position: insertPosition, + }; + }, + }); + +/** + * SLIDE_UPDATE - Update an existing slide + */ +export const createSlideUpdateTool = (_env: Env) => + createTool({ + id: "SLIDE_UPDATE", + description: + "Update an existing slide. Returns the updated slide content and manifest.", + inputSchema: z.object({ + manifest: z.string().describe("Current manifest.json content"), + slideId: z.string().describe("ID of the slide to update"), + currentSlideContent: z + .string() + .describe("Current JSON content of the slide file"), + updates: z + .object({ + layout: LayoutSchema.optional(), + title: z.string().optional(), + subtitle: z.string().optional(), + tag: z.string().optional(), + items: z.array(SlideItemSchema).optional(), + source: z.string().optional(), + label: z.string().optional(), + backgroundImage: z.string().optional(), + customHtml: z.string().optional(), + }) + .describe("Fields to update"), + }), + outputSchema: z.object({ + updatedSlideContent: z + .string() + .describe("Updated JSON content for the slide file"), + updatedManifest: z.string().describe("Updated manifest.json content"), + filename: z.string().describe("Filename of the updated slide"), + }), + execute: async ({ context }) => { + const { + manifest: manifestStr, + slideId, + currentSlideContent, + updates, + } = context; + const manifest = JSON.parse(manifestStr); + const currentSlide = JSON.parse(currentSlideContent); + + // Find slide in manifest + const slideIndex = manifest.slides?.findIndex( + (s: any) => s.id === slideId, + ); + if (slideIndex === -1 || slideIndex === undefined) { + throw new Error(`Slide with ID "${slideId}" not found in manifest`); + } + + // Merge updates with current slide + const updatedSlide = { + ...currentSlide, + ...updates, + id: slideId, // Preserve ID + }; + + // Update manifest entry if title or layout changed + if (updates.title || updates.layout) { + manifest.slides[slideIndex] = { + ...manifest.slides[slideIndex], + title: updates.title || manifest.slides[slideIndex].title, + layout: updates.layout || manifest.slides[slideIndex].layout, + }; + } + manifest.updatedAt = new Date().toISOString(); + + return { + updatedSlideContent: JSON.stringify(updatedSlide, null, 2), + updatedManifest: JSON.stringify(manifest, null, 2), + filename: manifest.slides[slideIndex].file, + }; + }, + }); + +/** + * SLIDE_DELETE - Delete a slide + */ +export const createSlideDeleteTool = (_env: Env) => + createTool({ + id: "SLIDE_DELETE", + description: + "Delete a slide from the deck. Returns the updated manifest and the filename to delete.", + inputSchema: z.object({ + manifest: z.string().describe("Current manifest.json content"), + slideId: z.string().describe("ID of the slide to delete"), + }), + outputSchema: z.object({ + updatedManifest: z.string().describe("Updated manifest.json content"), + deletedFilename: z.string().describe("Filename of the slide to delete"), + message: z.string().describe("Success message"), + }), + execute: async ({ context }) => { + const { manifest: manifestStr, slideId } = context; + const manifest = JSON.parse(manifestStr); + + // Find slide in manifest + const slideIndex = manifest.slides?.findIndex( + (s: any) => s.id === slideId, + ); + if (slideIndex === -1 || slideIndex === undefined) { + throw new Error(`Slide with ID "${slideId}" not found in manifest`); + } + + const deletedSlide = manifest.slides[slideIndex]; + manifest.slides.splice(slideIndex, 1); + manifest.updatedAt = new Date().toISOString(); + + return { + updatedManifest: JSON.stringify(manifest, null, 2), + deletedFilename: deletedSlide.file, + message: `Slide "${deletedSlide.title}" deleted successfully.`, + }; + }, + }); + +/** + * SLIDE_GET - Get a slide's data + */ +export const createSlideGetTool = (_env: Env) => + createTool({ + id: "SLIDE_GET", + description: "Parse and return structured data from a slide file.", + inputSchema: z.object({ + slideContent: z.string().describe("JSON content of the slide file"), + }), + outputSchema: SlideDataSchema, + execute: async ({ context }) => { + const slide = JSON.parse(context.slideContent); + return slide; + }, + }); + +/** + * SLIDE_LIST - List all slides in the deck + */ +export const createSlideListTool = (_env: Env) => + createTool({ + id: "SLIDE_LIST", + description: "List all slides in the deck with their metadata.", + inputSchema: z.object({ + manifest: z.string().describe("Content of manifest.json"), + }), + outputSchema: z.object({ + slides: z.array( + z.object({ + id: z.string(), + file: z.string(), + title: z.string(), + layout: z.string(), + position: z.number(), + }), + ), + total: z.number(), + }), + execute: async ({ context }) => { + const manifest = JSON.parse(context.manifest); + const slides = (manifest.slides || []).map((s: any, i: number) => ({ + id: s.id, + file: s.file, + title: s.title, + layout: s.layout, + position: i, + })); + + return { + slides, + total: slides.length, + }; + }, + }); + +/** + * SLIDE_REORDER - Reorder slides in the deck + */ +export const createSlideReorderTool = (_env: Env) => + createTool({ + id: "SLIDE_REORDER", + description: + "Reorder slides in the deck by moving a slide to a new position.", + inputSchema: z.object({ + manifest: z.string().describe("Current manifest.json content"), + slideId: z.string().describe("ID of the slide to move"), + newPosition: z + .number() + .describe("New position for the slide (0-indexed)"), + }), + outputSchema: z.object({ + updatedManifest: z.string().describe("Updated manifest.json content"), + message: z.string().describe("Success message"), + newOrder: z + .array( + z.object({ + id: z.string(), + title: z.string(), + position: z.number(), + }), + ) + .describe("New slide order"), + }), + execute: async ({ context }) => { + const { manifest: manifestStr, slideId, newPosition } = context; + const manifest = JSON.parse(manifestStr); + + if (!manifest.slides || manifest.slides.length === 0) { + throw new Error("No slides in deck"); + } + + // Find current position + const currentIndex = manifest.slides.findIndex( + (s: any) => s.id === slideId, + ); + if (currentIndex === -1) { + throw new Error(`Slide with ID "${slideId}" not found`); + } + + // Validate new position + const validPosition = Math.max( + 0, + Math.min(newPosition, manifest.slides.length - 1), + ); + + // Move slide + const [slide] = manifest.slides.splice(currentIndex, 1); + manifest.slides.splice(validPosition, 0, slide); + manifest.updatedAt = new Date().toISOString(); + + const newOrder = manifest.slides.map((s: any, i: number) => ({ + id: s.id, + title: s.title, + position: i, + })); + + return { + updatedManifest: JSON.stringify(manifest, null, 2), + message: `Slide "${slide.title}" moved to position ${validPosition + 1}.`, + newOrder, + }; + }, + }); + +/** + * SLIDE_DUPLICATE - Duplicate an existing slide + */ +export const createSlideDuplicateTool = (_env: Env) => + createTool({ + id: "SLIDE_DUPLICATE", + description: "Duplicate an existing slide. Creates a copy with a new ID.", + inputSchema: z.object({ + manifest: z.string().describe("Current manifest.json content"), + slideId: z.string().describe("ID of the slide to duplicate"), + slideContent: z + .string() + .describe("JSON content of the slide to duplicate"), + }), + outputSchema: z.object({ + slideFile: z.object({ + filename: z.string().describe("Filename for the new slide"), + content: z.string().describe("JSON content to write"), + }), + updatedManifest: z.string().describe("Updated manifest.json content"), + newSlideId: z.string().describe("ID of the new slide"), + position: z.number().describe("Position of the new slide"), + }), + execute: async ({ context }) => { + const { manifest: manifestStr, slideId, slideContent } = context; + const manifest = JSON.parse(manifestStr); + const originalSlide = JSON.parse(slideContent); + + // Find original slide position + const originalIndex = manifest.slides?.findIndex( + (s: any) => s.id === slideId, + ); + if (originalIndex === -1 || originalIndex === undefined) { + throw new Error(`Slide with ID "${slideId}" not found`); + } + + // Generate new ID and filename + const slideNum = String(manifest.slides.length + 1).padStart(3, "0"); + const newSlideId = `slide-${slideNum}-${Date.now()}`; + const slugTitle = originalSlide.title + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, "") + .substring(0, 20); + const filename = `${slideNum}-${slugTitle || originalSlide.layout}-copy.json`; + + // Create new slide with new ID + const newSlide = { + ...originalSlide, + id: newSlideId, + title: `${originalSlide.title} (Copy)`, + }; + + // Add to manifest after original + const manifestEntry = { + id: newSlideId, + file: filename, + title: newSlide.title, + layout: newSlide.layout, + }; + + const insertPosition = originalIndex + 1; + manifest.slides.splice(insertPosition, 0, manifestEntry); + manifest.updatedAt = new Date().toISOString(); + + return { + slideFile: { + filename, + content: JSON.stringify(newSlide, null, 2), + }, + updatedManifest: JSON.stringify(manifest, null, 2), + newSlideId, + position: insertPosition, + }; + }, + }); + +/** + * SLIDES_PREVIEW - Preview a complete presentation + * + * This tool displays slides in an interactive viewer UI (MCP App). + */ +export const createSlidesPreviewTool = (_env: Env) => + createTool({ + id: "SLIDES_PREVIEW", + _meta: { "ui/resourceUri": "ui://slides-viewer" }, + description: `Preview a complete slide presentation in an interactive viewer. + +Opens the slides viewer UI with navigation, keyboard controls, and thumbnails. + +**Use this to:** +- Show the user their presentation +- Preview slides before exporting +- Navigate through the deck + +**Input:** Array of slide objects (or JSON strings to parse)`, + inputSchema: z.object({ + title: z.string().optional().describe("Presentation title"), + slides: z + .array(z.unknown()) + .describe("Array of slide objects or JSON strings"), + }), + outputSchema: z.object({ + title: z.string(), + slides: z.array(z.unknown()), + slideCount: z.number(), + message: z.string(), + }), + execute: async ({ context }) => { + const { title = "Presentation", slides: rawSlides } = context; + + // Parse slides if they're strings + const slides = rawSlides.map((slide) => { + if (typeof slide === "string") { + try { + return JSON.parse(slide); + } catch { + return { title: "Parse Error", layout: "content" }; + } + } + return slide; + }); + + return { + title, + slides, + slideCount: slides.length, + message: `Showing ${slides.length} slides. Use arrow keys or buttons to navigate.`, + }; + }, + }); + +// Export all slide tools +export const slideTools = [ + createSlideCreateTool, + createSlideUpdateTool, + createSlideDeleteTool, + createSlideGetTool, + createSlideListTool, + createSlideReorderTool, + createSlideDuplicateTool, + createSlidesPreviewTool, +]; diff --git a/slides/server/tools/style.ts b/slides/server/tools/style.ts new file mode 100644 index 00000000..1025fac8 --- /dev/null +++ b/slides/server/tools/style.ts @@ -0,0 +1,230 @@ +/** + * Style guide management tools for slide presentations. + * + * These tools handle reading and updating the style.md file that + * defines the visual style, tone, and design system for presentations. + */ +import { createTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../types/env.ts"; + +/** + * STYLE_GET - Get the current style guide content + */ +export const createStyleGetTool = (_env: Env) => + createTool({ + id: "STYLE_GET", + description: + "Get the current style guide content. The style.md file defines the visual style, tone, and design system for the presentation.", + inputSchema: z.object({ + content: z.string().describe("Current content of the style.md file"), + }), + outputSchema: z.object({ + styleGuide: z.string().describe("The style guide content"), + sections: z + .array( + z.object({ + heading: z.string(), + content: z.string(), + }), + ) + .optional() + .describe("Parsed sections of the style guide"), + }), + execute: async ({ context }) => { + const { content } = context; + + // Parse sections from markdown + const lines = content.split("\n"); + const sections: { heading: string; content: string }[] = []; + let currentHeading = ""; + let currentContent: string[] = []; + + for (const line of lines) { + if (line.startsWith("## ")) { + if (currentHeading) { + sections.push({ + heading: currentHeading, + content: currentContent.join("\n").trim(), + }); + } + currentHeading = line.replace("## ", "").trim(); + currentContent = []; + } else if (currentHeading) { + currentContent.push(line); + } + } + + // Add last section + if (currentHeading) { + sections.push({ + heading: currentHeading, + content: currentContent.join("\n").trim(), + }); + } + + return { + styleGuide: content, + sections, + }; + }, + }); + +/** + * STYLE_SET - Update the style guide content + */ +export const createStyleSetTool = (_env: Env) => + createTool({ + id: "STYLE_SET", + description: + "Update the style guide content. This defines the visual style, tone, and design system for the presentation. Returns the new content to write to style.md.", + inputSchema: z.object({ + content: z + .string() + .describe("New style guide content in markdown format"), + }), + outputSchema: z.object({ + content: z + .string() + .describe("The style guide content to write to style.md"), + message: z.string().describe("Success message"), + }), + execute: async ({ context }) => { + const { content } = context; + + // Validate that it looks like a style guide + if (!content.includes("#") && content.length < 50) { + throw new Error( + "Style guide should be in markdown format with sections (using # or ## headings)", + ); + } + + return { + content, + message: "Style guide updated successfully.", + }; + }, + }); + +/** + * STYLE_SUGGEST - Generate style guide suggestions based on presentation topic + */ +export const createStyleSuggestTool = (_env: Env) => + createTool({ + id: "STYLE_SUGGEST", + description: + "Generate style guide suggestions based on the presentation topic and purpose.", + inputSchema: z.object({ + topic: z.string().describe("The main topic or theme of the presentation"), + purpose: z + .enum(["investor", "sales", "educational", "internal", "conference"]) + .describe("The purpose of the presentation"), + tone: z + .enum(["formal", "casual", "technical", "inspirational"]) + .optional() + .default("formal") + .describe("Desired tone of the presentation"), + }), + outputSchema: z.object({ + suggestedStyle: z.string().describe("Suggested style guide content"), + recommendations: z + .array(z.string()) + .describe("Key recommendations for this type of presentation"), + }), + execute: async ({ context }) => { + const { topic, purpose, tone } = context; + + const toneDescriptions = { + formal: "Professional and polished, suitable for executive audiences", + casual: "Relaxed and approachable, with conversational language", + technical: "Detailed and precise, with technical terminology", + inspirational: "Motivational and engaging, with powerful statements", + }; + + const purposeLayouts = { + investor: ["title", "stats", "timeline", "two-column", "list"], + sales: ["title", "stats", "image", "quote", "list"], + educational: ["title", "content", "two-column", "list", "image"], + internal: ["title", "content", "list", "stats", "two-column"], + conference: ["title", "image", "quote", "stats", "content"], + }; + + const recommendations = { + investor: [ + "Lead with key metrics and growth numbers", + "Use timeline slides to show company journey", + "Include clear asks and next steps", + "Keep slides data-driven but not cluttered", + ], + sales: [ + "Focus on customer value and outcomes", + "Include testimonials or case studies", + "Use stats to build credibility", + "End with clear call to action", + ], + educational: [ + "Break complex topics into digestible chunks", + "Use examples and illustrations", + "Include key takeaways on each section", + "Maintain consistent terminology", + ], + internal: [ + "Be transparent about challenges and solutions", + "Include actionable next steps", + "Credit team contributions", + "Keep it focused and time-efficient", + ], + conference: [ + "Start with a hook that captures attention", + "Use large, readable text for distant audiences", + "Include memorable quotes or statistics", + "End with a strong, shareable takeaway", + ], + }; + + const suggestedStyle = `# ${topic} - Presentation Style Guide + +## Purpose & Audience +${purpose.charAt(0).toUpperCase() + purpose.slice(1)} presentation +${toneDescriptions[tone]} + +## Brand Colors + +- **Primary Accent**: Green (#c4df1b) - Used for emphasis and key points +- **Secondary Accent**: Purple (#d4a5ff) - Used for alternative sections +- **Tertiary Accent**: Yellow (#ffd666) - Used for highlights + +## Typography + +- **Headlines**: Large, bold, with tight letter-spacing +- **Body Text**: Clean, readable, 17-18px +- **Monospace**: For tags and technical content + +## Recommended Slide Layouts + +${purposeLayouts[purpose].map((l, i) => `${i + 1}. **${l}** - Use for ${l === "title" ? "section breaks" : l === "stats" ? "key metrics" : l === "image" ? "visual impact" : l === "quote" ? "testimonials" : l === "list" ? "action items" : "detailed content"}`).join("\n")} + +## Tone & Voice + +- ${toneDescriptions[tone]} +- Clear, concise language +- ${purpose === "investor" ? "Data-driven with strategic context" : purpose === "sales" ? "Value-focused and persuasive" : purpose === "educational" ? "Explanatory and progressive" : purpose === "conference" ? "Engaging and memorable" : "Direct and actionable"} + +## Best Practices + +${recommendations[purpose].map((r) => `- ${r}`).join("\n")} +`; + + return { + suggestedStyle, + recommendations: recommendations[purpose], + }; + }, + }); + +// Export all style tools +export const styleTools = [ + createStyleGetTool, + createStyleSetTool, + createStyleSuggestTool, +]; diff --git a/slides/server/types/env.ts b/slides/server/types/env.ts new file mode 100644 index 00000000..857a7779 --- /dev/null +++ b/slides/server/types/env.ts @@ -0,0 +1,17 @@ +/** + * Environment Type Definitions for Slides MCP + */ +import { + BindingOf, + type DefaultEnv, + type BindingRegistry, +} from "@decocms/runtime"; +import { z } from "zod"; + +export const StateSchema = z.object({ + BRAND: BindingOf("@deco/brand") + .optional() + .describe("Brand research - any MCP with BRAND tools"), +}); + +export type Env = DefaultEnv; diff --git a/slides/test-presentation/engine.js b/slides/test-presentation/engine.js new file mode 100644 index 00000000..ddb58333 --- /dev/null +++ b/slides/test-presentation/engine.js @@ -0,0 +1,1154 @@ +/** + * Slides Presentation Engine + * Adapted from deco.cx Investor Updates presentation + * Works with CDN-loaded React and GSAP - no build required + */ + +// Base dimensions (16:9 aspect ratio) +const BASE_WIDTH = 1920; +const BASE_HEIGHT = 1080; + +// Color system +const colors = { + // Background colors + bg: { + "dc-950": "#0f0e0d", + "dc-900": "#1a1918", + "dc-800": "#2a2927", + "primary-light": "#c4df1b", + "purple-light": "#d4a5ff", + "yellow-light": "#ffd666", + }, + // Text colors + text: { + "dc-50": "#faf9f7", + "dc-100": "#f0eeeb", + "dc-200": "#e1ddd8", + "dc-300": "#c9c4bc", + "dc-400": "#a39d94", + "dc-500": "#6d6a66", + "dc-600": "#52504c", + "dc-700": "#3a3936", + "dc-800": "#2a2927", + "dc-900": "#1a1918", + }, + // Accent colors + accent: { + green: "#c4df1b", + purple: "#d4a5ff", + yellow: "#ffd666", + }, +}; + +// CSS class mappings +const bgColorMap = { + "dc-950": "bg-dc-950", + "dc-900": "bg-dc-900", + "dc-800": "bg-dc-800", + "primary-light": "bg-primary-light", + "purple-light": "bg-purple-light", + "yellow-light": "bg-yellow-light", +}; + +const accentTextClass = { + green: "text-accent-green", + purple: "text-accent-purple", + yellow: "text-accent-yellow", +}; + +const accentBgClass = { + green: "bg-accent-green", + purple: "bg-accent-purple", + yellow: "bg-accent-yellow", +}; + +/** + * Main Presentation Component + */ +function Presentation({ slides, title, subtitle, styleGuide }) { + const [currentSlide, setCurrentSlide] = React.useState(0); + const [isAnimating, setIsAnimating] = React.useState(false); + const [scale, setScale] = React.useState(1); + const [isFullscreen, setIsFullscreen] = React.useState(false); + const containerRef = React.useRef(null); + const slideRefs = React.useRef([]); + + const totalSlides = slides.length + 1; // +1 for cover slide + + // Calculate scale based on viewport + React.useEffect(() => { + const updateScale = () => { + const vw = window.innerWidth; + const vh = window.innerHeight; + const scaleX = vw / BASE_WIDTH; + const scaleY = vh / BASE_HEIGHT; + setScale(Math.min(scaleX, scaleY)); + }; + + updateScale(); + window.addEventListener("resize", updateScale); + return () => window.removeEventListener("resize", updateScale); + }, []); + + // Keyboard navigation + React.useEffect(() => { + const handleKeyDown = (e) => { + if (isAnimating) return; + + switch (e.key) { + case "ArrowRight": + case "ArrowDown": + case " ": + e.preventDefault(); + goToNextSlide(); + break; + case "ArrowLeft": + case "ArrowUp": + e.preventDefault(); + goToPrevSlide(); + break; + case "Home": + e.preventDefault(); + goToSlide(0); + break; + case "End": + e.preventDefault(); + goToSlide(totalSlides - 1); + break; + case "f": + case "F": + e.preventDefault(); + toggleFullscreen(); + break; + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [isAnimating, currentSlide, totalSlides]); + + // GSAP slide transition + const animateSlideTransition = async (fromIndex, toIndex) => { + if (typeof gsap === "undefined") { + setCurrentSlide(toIndex); + return; + } + + setIsAnimating(true); + + // Animate out current slide + const currentEl = slideRefs.current[fromIndex]; + if (currentEl) { + const items = currentEl.querySelectorAll(".animate-item"); + await gsap.to(items, { + y: -20, + opacity: 0, + duration: 0.2, + stagger: 0.02, + ease: "power2.in", + }); + } + + // Update slide + setCurrentSlide(toIndex); + + // Wait for React to render + await new Promise((r) => setTimeout(r, 50)); + + // Animate in new slide + const newEl = slideRefs.current[toIndex]; + if (newEl) { + const items = newEl.querySelectorAll(".animate-item"); + gsap.set(items, { y: 40, opacity: 0 }); + await gsap.to(items, { + y: 0, + opacity: 1, + duration: 0.4, + stagger: 0.05, + ease: "power2.out", + }); + } + + setIsAnimating(false); + }; + + const goToSlide = (index) => { + if ( + isAnimating || + index === currentSlide || + index < 0 || + index >= totalSlides + ) + return; + animateSlideTransition(currentSlide, index); + }; + + const goToNextSlide = () => { + if (currentSlide < totalSlides - 1) { + goToSlide(currentSlide + 1); + } + }; + + const goToPrevSlide = () => { + if (currentSlide > 0) { + goToSlide(currentSlide - 1); + } + }; + + const toggleFullscreen = () => { + if (!document.fullscreenElement) { + containerRef.current?.requestFullscreen(); + setIsFullscreen(true); + } else { + document.exitFullscreen(); + setIsFullscreen(false); + } + }; + + // Touch gesture handling + const touchStartRef = React.useRef({ x: 0, y: 0 }); + + const handleTouchStart = (e) => { + touchStartRef.current = { + x: e.touches[0].clientX, + y: e.touches[0].clientY, + }; + }; + + const handleTouchEnd = (e) => { + const deltaX = e.changedTouches[0].clientX - touchStartRef.current.x; + const deltaY = e.changedTouches[0].clientY - touchStartRef.current.y; + + if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 50) { + if (deltaX < 0) { + goToNextSlide(); + } else { + goToPrevSlide(); + } + } + }; + + // Render slide content based on layout + const renderSlideContent = (slide, index, isActive) => { + const bgClass = + bgColorMap[slide.backgroundColor || "dc-950"] || "bg-dc-950"; + const textColorClass = + slide.textColor === "dark" ? "text-dc-900" : "text-dc-50"; + const accent = slide.accent || "green"; + + switch (slide.layout) { + case "title": + return renderTitleSlide(slide, bgClass, textColorClass, accent); + case "content": + return renderContentSlide(slide, bgClass, textColorClass, accent); + case "two-column": + return renderTwoColumnSlide(slide, bgClass, textColorClass, accent); + case "stats": + return renderStatsSlide( + slide, + bgClass, + textColorClass, + accent, + isActive, + ); + case "list": + return renderListSlide(slide, bgClass, textColorClass, accent); + case "image": + return renderImageSlide(slide, bgClass, textColorClass, accent); + case "quote": + return renderQuoteSlide(slide, bgClass, textColorClass, accent); + case "custom": + return renderCustomSlide(slide, bgClass, textColorClass); + default: + return renderTitleSlide(slide, bgClass, textColorClass, accent); + } + }; + + // Title slide layout + const renderTitleSlide = (slide, bgClass, textColorClass, accent) => + React.createElement( + "div", + { + className: `w-full h-full flex flex-col ${bgClass} ${textColorClass}`, + style: { padding: "64px 80px" }, + }, + slide.tag && + React.createElement( + "span", + { + className: + "animate-item font-mono uppercase tracking-widest opacity-50", + style: { + fontSize: "11px", + marginBottom: "16px", + letterSpacing: "0.2em", + }, + }, + slide.tag, + ), + React.createElement( + "div", + { className: "flex-1 flex flex-col justify-end" }, + React.createElement( + "h1", + { + className: "animate-item leading-none", + style: { fontSize: "180px", letterSpacing: "-4px" }, + }, + slide.title, + ), + slide.subtitle && + React.createElement( + "p", + { + className: "animate-item opacity-50", + style: { fontSize: "24px", marginTop: "24px" }, + }, + slide.subtitle, + ), + ), + ); + + // Content slide layout + const renderContentSlide = (slide, bgClass, textColorClass, accent) => + React.createElement( + "div", + { + className: `w-full h-full flex flex-col ${bgClass} ${textColorClass}`, + style: { padding: "80px 96px" }, + }, + React.createElement( + "div", + { style: { marginBottom: "72px" } }, + slide.tag && + React.createElement( + "span", + { + className: + "animate-item font-mono uppercase tracking-widest text-dc-500", + style: { + fontSize: "12px", + marginBottom: "16px", + display: "block", + letterSpacing: "0.2em", + }, + }, + slide.tag, + ), + React.createElement( + "h2", + { + className: "animate-item text-dc-200", + style: { + fontSize: "32px", + letterSpacing: "-0.5px", + lineHeight: 1.2, + }, + }, + slide.title, + ), + ), + slide.items && + React.createElement( + "div", + { + className: "flex-1 flex flex-col", + style: { gap: "48px" }, + }, + slide.items.map((item, i) => + React.createElement( + "div", + { key: i, className: "animate-item" }, + item.title && + React.createElement( + "h3", + { + className: "text-dc-300", + style: { fontSize: "18px", marginBottom: "20px" }, + }, + item.title, + ), + item.bullets && + React.createElement( + "ul", + { + style: { + display: "flex", + flexDirection: "column", + gap: "16px", + }, + }, + item.bullets.map((bullet, j) => + React.createElement( + "li", + { + key: j, + className: "flex items-start", + style: { + fontSize: "17px", + gap: "16px", + lineHeight: 1.5, + }, + }, + React.createElement( + "span", + { + className: accentTextClass[accent], + style: { marginTop: "6px", fontSize: "8px" }, + }, + "●", + ), + React.createElement( + "span", + { + className: bullet.highlight + ? accentTextClass[accent] + : "text-dc-300", + }, + bullet.text, + ), + ), + ), + ), + ), + ), + ), + ); + + // Two-column slide layout + const renderTwoColumnSlide = (slide, bgClass, textColorClass, accent) => + React.createElement( + "div", + { + className: `w-full h-full flex flex-col ${bgClass} ${textColorClass}`, + style: { padding: "80px 96px" }, + }, + React.createElement( + "div", + { style: { marginBottom: "72px" } }, + slide.tag && + React.createElement( + "span", + { + className: + "animate-item font-mono uppercase tracking-widest text-dc-500", + style: { + fontSize: "12px", + marginBottom: "16px", + display: "block", + letterSpacing: "0.2em", + }, + }, + slide.tag, + ), + React.createElement( + "h2", + { + className: "animate-item text-dc-200", + style: { + fontSize: "32px", + letterSpacing: "-0.5px", + lineHeight: 1.2, + }, + }, + slide.title, + ), + slide.subtitle && + React.createElement( + "p", + { + className: "animate-item text-dc-400", + style: { fontSize: "17px", marginTop: "16px" }, + }, + slide.subtitle, + ), + ), + slide.items && + React.createElement( + "div", + { + className: "flex-1 grid", + style: { gridTemplateColumns: "1fr 1fr", gap: "80px" }, + }, + slide.items.map((item, i) => + React.createElement( + "div", + { key: i, className: "animate-item" }, + item.title && + React.createElement( + "h3", + { + className: accentTextClass[accent], + style: { fontSize: "18px", marginBottom: "24px" }, + }, + item.title, + ), + item.bullets && + React.createElement( + "ul", + { + style: { + display: "flex", + flexDirection: "column", + gap: "14px", + }, + }, + item.bullets.map((bullet, j) => + React.createElement( + "li", + { + key: j, + className: bullet.highlight + ? accentTextClass[accent] + : "text-dc-400", + style: { fontSize: "15px", lineHeight: 1.5 }, + }, + bullet.text, + ), + ), + ), + ), + ), + ), + ); + + // Stats slide layout with count-up animation + const renderStatsSlide = (slide, bgClass, textColorClass, accent, isActive) => + React.createElement( + "div", + { + className: `w-full h-full flex flex-col ${bgClass} ${textColorClass}`, + style: { padding: "80px 96px" }, + }, + React.createElement( + "div", + { style: { marginBottom: "80px" } }, + slide.tag && + React.createElement( + "span", + { + className: + "animate-item font-mono uppercase tracking-widest text-dc-500", + style: { + fontSize: "12px", + marginBottom: "16px", + display: "block", + letterSpacing: "0.2em", + }, + }, + slide.tag, + ), + React.createElement( + "h2", + { + className: "animate-item text-dc-200", + style: { + fontSize: "32px", + letterSpacing: "-0.5px", + lineHeight: 1.2, + }, + }, + slide.title, + ), + ), + slide.items && + React.createElement( + "div", + { + className: "flex-1 grid items-center", + style: { + gridTemplateColumns: `repeat(${Math.min(slide.items.length, 4)}, 1fr)`, + gap: "64px", + }, + }, + slide.items.map((item, i) => + React.createElement( + "div", + { key: i, className: "animate-item text-center" }, + item.value && + React.createElement(CountUp, { + end: parseInt(item.value.replace(/[^0-9]/g, "")) || 0, + prefix: item.value.match(/^[^0-9]*/)?.[0] || "", + suffix: item.value.match(/[^0-9]*$/)?.[0] || "", + isActive: isActive, + className: `block ${accentTextClass[accent]}`, + style: { + fontSize: "56px", + marginBottom: "16px", + letterSpacing: "-1px", + }, + }), + item.label && + React.createElement( + "span", + { + className: "text-dc-400", + style: { fontSize: "16px" }, + }, + item.label, + ), + ), + ), + ), + ); + + // List slide layout + const renderListSlide = (slide, bgClass, textColorClass, accent) => + React.createElement( + "div", + { + className: `w-full h-full flex flex-col ${bgClass} ${textColorClass}`, + style: { padding: "80px 96px" }, + }, + React.createElement( + "div", + { style: { marginBottom: "72px" } }, + slide.tag && + React.createElement( + "span", + { + className: + "animate-item font-mono uppercase tracking-widest text-dc-500", + style: { + fontSize: "12px", + marginBottom: "16px", + display: "block", + letterSpacing: "0.2em", + }, + }, + slide.tag, + ), + React.createElement( + "h2", + { + className: "animate-item text-dc-200", + style: { + fontSize: "32px", + letterSpacing: "-0.5px", + lineHeight: 1.2, + }, + }, + slide.title, + ), + slide.subtitle && + React.createElement( + "p", + { + className: "animate-item text-dc-400", + style: { fontSize: "17px", marginTop: "16px", maxWidth: "800px" }, + }, + slide.subtitle, + ), + ), + slide.items && + React.createElement( + "div", + { + className: "flex-1 grid", + style: { + gridTemplateColumns: "1fr 1fr", + columnGap: "80px", + rowGap: "40px", + }, + }, + slide.items.map((item, i) => + React.createElement( + "div", + { + key: i, + className: "animate-item flex items-start", + style: { gap: "20px" }, + }, + React.createElement("div", { + className: accentBgClass[accent], + style: { + width: "6px", + height: "6px", + borderRadius: "50%", + marginTop: "8px", + flexShrink: 0, + }, + }), + React.createElement( + "div", + null, + item.title && + React.createElement( + "span", + { + className: "text-dc-100 block", + style: { fontSize: "17px", lineHeight: 1.5 }, + }, + item.title, + ), + item.subtitle && + React.createElement( + "span", + { + className: "text-dc-500 block", + style: { fontSize: "15px", marginTop: "6px" }, + }, + item.subtitle, + ), + ), + ), + ), + ), + ); + + // Image slide layout + const renderImageSlide = (slide, bgClass, textColorClass, accent) => + React.createElement( + "div", + { + className: `w-full h-full relative ${bgClass} ${textColorClass}`, + style: { overflow: "hidden" }, + }, + slide.backgroundImage && + React.createElement("img", { + src: slide.backgroundImage, + alt: "", + style: { + position: "absolute", + inset: 0, + width: "100%", + height: "100%", + objectFit: "cover", + }, + }), + React.createElement("div", { + style: { + position: "absolute", + inset: 0, + background: + "linear-gradient(to top, rgba(15,14,13,0.9) 0%, rgba(15,14,13,0.3) 50%, transparent 100%)", + }, + }), + React.createElement( + "div", + { + className: "absolute bottom-0 left-0 right-0", + style: { padding: "80px 96px" }, + }, + slide.tag && + React.createElement( + "span", + { + className: + "animate-item font-mono uppercase tracking-widest text-dc-400", + style: { + fontSize: "12px", + marginBottom: "16px", + display: "block", + letterSpacing: "0.2em", + }, + }, + slide.tag, + ), + React.createElement( + "h2", + { + className: "animate-item text-dc-50", + style: { + fontSize: "48px", + letterSpacing: "-1px", + lineHeight: 1.2, + maxWidth: "900px", + }, + }, + slide.title, + ), + slide.subtitle && + React.createElement( + "p", + { + className: "animate-item text-dc-300", + style: { fontSize: "20px", marginTop: "24px", maxWidth: "700px" }, + }, + slide.subtitle, + ), + ), + ); + + // Quote slide layout + const renderQuoteSlide = (slide, bgClass, textColorClass, accent) => + React.createElement( + "div", + { + className: `w-full h-full flex flex-col items-center justify-center ${bgClass} ${textColorClass}`, + style: { padding: "96px" }, + }, + React.createElement( + "blockquote", + { + className: "animate-item text-center", + style: { maxWidth: "1200px" }, + }, + React.createElement( + "span", + { + className: accentTextClass[accent], + style: { + fontSize: "120px", + lineHeight: 0.5, + display: "block", + marginBottom: "24px", + }, + }, + '"', + ), + React.createElement( + "p", + { + style: { + fontSize: "42px", + lineHeight: 1.4, + letterSpacing: "-0.5px", + }, + }, + slide.title, + ), + slide.subtitle && + React.createElement( + "cite", + { + className: "text-dc-400 block not-italic", + style: { fontSize: "18px", marginTop: "48px" }, + }, + "— ", + slide.subtitle, + ), + ), + ); + + // Custom HTML slide layout + const renderCustomSlide = (slide, bgClass, textColorClass) => + React.createElement("div", { + className: `w-full h-full ${bgClass} ${textColorClass}`, + dangerouslySetInnerHTML: { __html: slide.customHtml || "" }, + }); + + // Cover slide + const renderCoverSlide = () => + React.createElement( + "div", + { + className: + "w-full h-full flex flex-col justify-between bg-dc-950 text-dc-50", + style: { padding: "90px 82px" }, + }, + React.createElement( + "div", + null, + React.createElement( + "div", + { + className: "animate-item", + style: { fontSize: "32px", fontWeight: 600 }, + }, + "◆", + ), + ), + React.createElement( + "div", + { + className: "flex flex-col", + style: { gap: "22px", maxWidth: "1175px" }, + }, + subtitle && + React.createElement( + "p", + { + className: "animate-item font-mono uppercase text-dc-400", + style: { fontSize: "24px", letterSpacing: "1.2px" }, + }, + subtitle, + ), + React.createElement( + "h1", + { + className: "animate-item text-dc-50 leading-none", + style: { fontSize: "140px", letterSpacing: "-2.8px" }, + }, + title || "Presentation", + ), + ), + ); + + // Calculate display dimensions + const displayWidth = BASE_WIDTH * scale; + const displayHeight = BASE_HEIGHT * scale; + + return React.createElement( + "div", + { + ref: containerRef, + className: "fixed inset-0 w-screen h-screen bg-dc-950 overflow-hidden", + onTouchStart: handleTouchStart, + onTouchEnd: handleTouchEnd, + }, + // Hide animate-item elements initially + React.createElement( + "style", + null, + ` + .animate-item { + opacity: 0; + transform: translateY(40px); + } + `, + ), + + // Centered container for scaled presentation + React.createElement( + "div", + { + className: "absolute inset-0 flex items-center justify-center", + }, + React.createElement( + "div", + { + style: { + width: `${displayWidth}px`, + height: `${displayHeight}px`, + position: "relative", + overflow: "hidden", + }, + }, + // Scaled presentation container + React.createElement( + "div", + { + style: { + width: `${BASE_WIDTH}px`, + height: `${BASE_HEIGHT}px`, + transform: `scale(${scale})`, + transformOrigin: "top left", + position: "absolute", + top: 0, + left: 0, + }, + }, + // Cover slide + React.createElement( + "div", + { + ref: (el) => (slideRefs.current[0] = el), + className: `absolute inset-0 transition-opacity duration-300 ${ + currentSlide === 0 + ? "opacity-100 pointer-events-auto" + : "opacity-0 pointer-events-none" + }`, + }, + renderCoverSlide(), + ), + + // Content slides + slides.map((slide, index) => + React.createElement( + "div", + { + key: index, + ref: (el) => (slideRefs.current[index + 1] = el), + className: `absolute inset-0 transition-opacity duration-300 ${ + currentSlide === index + 1 + ? "opacity-100 pointer-events-auto" + : "opacity-0 pointer-events-none" + }`, + }, + renderSlideContent(slide, index, currentSlide === index + 1), + ), + ), + + // Navigation controls + React.createElement( + "div", + { + className: "absolute flex items-center z-50", + style: { bottom: "40px", right: "48px", gap: "12px" }, + }, + // Slide counter + React.createElement( + "span", + { + className: "text-dc-600 font-mono", + style: { fontSize: "12px", marginRight: "12px" }, + }, + `${String(currentSlide + 1).padStart(2, "0")} / ${String(totalSlides).padStart(2, "0")}`, + ), + + // First button + React.createElement( + "button", + { + type: "button", + onClick: () => goToSlide(0), + disabled: currentSlide === 0 || isAnimating, + className: `rounded-full flex items-center justify-center transition-all border ${ + currentSlide === 0 + ? "border-dc-800 text-dc-700 cursor-not-allowed" + : "border-dc-700 hover:border-dc-600 text-dc-400 hover:text-dc-300 cursor-pointer" + }`, + style: { width: "36px", height: "36px" }, + }, + "⏮", + ), + + // Previous button + React.createElement( + "button", + { + type: "button", + onClick: goToPrevSlide, + disabled: currentSlide === 0 || isAnimating, + className: `rounded-full flex items-center justify-center transition-all border ${ + currentSlide === 0 + ? "border-dc-800 text-dc-700 cursor-not-allowed" + : "border-dc-700 hover:border-dc-600 text-dc-400 hover:text-dc-300 cursor-pointer" + }`, + style: { width: "36px", height: "36px" }, + }, + "←", + ), + + // Next button + React.createElement( + "button", + { + type: "button", + onClick: goToNextSlide, + disabled: currentSlide === totalSlides - 1 || isAnimating, + className: `rounded-full flex items-center justify-center transition-all ${ + currentSlide === totalSlides - 1 + ? "border border-dc-800 text-dc-700 cursor-not-allowed" + : "bg-primary-light/10 border border-primary-light/30 hover:bg-primary-light/20 text-primary-light cursor-pointer" + }`, + style: { width: "36px", height: "36px" }, + }, + "→", + ), + + // Fullscreen button + React.createElement( + "button", + { + type: "button", + onClick: toggleFullscreen, + className: + "rounded-full flex items-center justify-center transition-all border border-dc-700 hover:border-dc-600 text-dc-400 hover:text-dc-300 cursor-pointer", + style: { width: "36px", height: "36px", marginLeft: "8px" }, + title: isFullscreen ? "Exit fullscreen" : "Enter fullscreen", + }, + isFullscreen ? "⛶" : "⛶", + ), + ), + ), + ), + ), + ); +} + +/** + * CountUp Component for stats animation + */ +function CountUp({ + end, + prefix = "", + suffix = "", + duration = 2000, + className = "", + style = {}, + isActive = false, +}) { + const [count, setCount] = React.useState(0); + const hasAnimatedRef = React.useRef(false); + + React.useEffect(() => { + if (isActive && !hasAnimatedRef.current) { + hasAnimatedRef.current = true; + const startTime = Date.now(); + + const animate = () => { + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + // Ease out cubic + const eased = 1 - Math.pow(1 - progress, 3); + setCount(Math.floor(eased * end)); + + if (progress < 1) { + requestAnimationFrame(animate); + } else { + setCount(end); + } + }; + + requestAnimationFrame(animate); + } + }, [isActive, end, duration]); + + // Reset when becoming inactive + React.useEffect(() => { + if (!isActive) { + hasAnimatedRef.current = false; + setCount(0); + } + }, [isActive]); + + return React.createElement( + "span", + { className, style }, + prefix, + count.toLocaleString(), + suffix, + ); +} + +/** + * Initialize presentation from manifest + */ +async function initPresentation(manifestPath = "./slides/manifest.json") { + try { + const response = await fetch(manifestPath); + const manifest = await response.json(); + + // Load all slides + const slides = await Promise.all( + manifest.slides.map(async (slideInfo) => { + try { + const slideResponse = await fetch(`./slides/${slideInfo.file}`); + const slideData = await slideResponse.json(); + return { ...slideInfo, ...slideData }; + } catch (e) { + console.error(`Failed to load slide: ${slideInfo.file}`, e); + return slideInfo; + } + }), + ); + + // Render presentation + const root = ReactDOM.createRoot(document.getElementById("root")); + root.render( + React.createElement(Presentation, { + slides, + title: manifest.title || "Presentation", + subtitle: manifest.subtitle || "", + styleGuide: manifest.styleGuide || "", + }), + ); + } catch (e) { + console.error("Failed to initialize presentation:", e); + document.getElementById("root").innerHTML = ` +
+

Presentation not found

+

+ Create a manifest.json in the slides directory to get started. +

+
+ `; + } +} + +// Export for use in HTML +window.Presentation = Presentation; +window.CountUp = CountUp; +window.initPresentation = initPresentation; diff --git a/slides/test-presentation/index.html b/slides/test-presentation/index.html new file mode 100644 index 00000000..5df3b001 --- /dev/null +++ b/slides/test-presentation/index.html @@ -0,0 +1,22 @@ + + + + + + Test Presentation + + + + + + + + +
Loading presentation
+ + + diff --git a/slides/test-presentation/slides/001-introduction.json b/slides/test-presentation/slides/001-introduction.json new file mode 100644 index 00000000..9e3c11c3 --- /dev/null +++ b/slides/test-presentation/slides/001-introduction.json @@ -0,0 +1,10 @@ +{ + "id": "slide-001", + "layout": "title", + "title": "Introduction", + "subtitle": "Getting started with Slides MCP", + "tag": "OVERVIEW", + "backgroundColor": "dc-950", + "textColor": "light", + "accent": "green" +} diff --git a/slides/test-presentation/slides/002-key-metrics.json b/slides/test-presentation/slides/002-key-metrics.json new file mode 100644 index 00000000..90c41759 --- /dev/null +++ b/slides/test-presentation/slides/002-key-metrics.json @@ -0,0 +1,15 @@ +{ + "id": "slide-002", + "layout": "stats", + "title": "Key Metrics", + "tag": "PERFORMANCE", + "backgroundColor": "dc-950", + "textColor": "light", + "accent": "green", + "items": [ + { "value": "2.5M", "label": "Users" }, + { "value": "99.9%", "label": "Uptime" }, + { "value": "150ms", "label": "Response Time" }, + { "value": "$1.2M", "label": "Revenue" } + ] +} diff --git a/slides/test-presentation/slides/003-features.json b/slides/test-presentation/slides/003-features.json new file mode 100644 index 00000000..f1ca3d0f --- /dev/null +++ b/slides/test-presentation/slides/003-features.json @@ -0,0 +1,18 @@ +{ + "id": "slide-003", + "layout": "list", + "title": "Key Features", + "subtitle": "Everything you need to create beautiful presentations", + "tag": "FEATURES", + "backgroundColor": "dc-950", + "textColor": "light", + "accent": "green", + "items": [ + { "title": "Multiple Layouts", "subtitle": "Title, content, stats, two-column, list, quote, image, and custom" }, + { "title": "GSAP Animations", "subtitle": "Smooth, professional transitions powered by GSAP" }, + { "title": "No Build Required", "subtitle": "Uses CDN-loaded React and GSAP - just serve and present" }, + { "title": "Portable Output", "subtitle": "Bundle to a single HTML file that works anywhere" }, + { "title": "Style Guide", "subtitle": "Define your design system in a simple markdown file" }, + { "title": "AI-Powered", "subtitle": "Create presentations through natural conversation" } + ] +} diff --git a/slides/test-presentation/slides/004-comparison.json b/slides/test-presentation/slides/004-comparison.json new file mode 100644 index 00000000..129619e5 --- /dev/null +++ b/slides/test-presentation/slides/004-comparison.json @@ -0,0 +1,30 @@ +{ + "id": "slide-004", + "layout": "two-column", + "title": "Before vs After", + "subtitle": "The transformation with Slides MCP", + "tag": "COMPARISON", + "backgroundColor": "dc-950", + "textColor": "light", + "accent": "purple", + "items": [ + { + "title": "Traditional Approach", + "bullets": [ + { "text": "Manual slide creation in PowerPoint" }, + { "text": "Time-consuming formatting" }, + { "text": "Inconsistent styling across slides" }, + { "text": "Difficult to version control" } + ] + }, + { + "title": "With Slides MCP", + "bullets": [ + { "text": "AI-assisted content creation", "highlight": true }, + { "text": "Automatic beautiful formatting", "highlight": true }, + { "text": "Consistent design system", "highlight": true }, + { "text": "Git-friendly JSON files", "highlight": true } + ] + } + ] +} diff --git a/slides/test-presentation/slides/005-quote.json b/slides/test-presentation/slides/005-quote.json new file mode 100644 index 00000000..4955b112 --- /dev/null +++ b/slides/test-presentation/slides/005-quote.json @@ -0,0 +1,9 @@ +{ + "id": "slide-005", + "layout": "quote", + "title": "This is the future of presentation creation. I can't imagine going back to the old way.", + "subtitle": "Happy User, CEO of Example Corp", + "backgroundColor": "dc-950", + "textColor": "light", + "accent": "yellow" +} diff --git a/slides/test-presentation/slides/manifest.json b/slides/test-presentation/slides/manifest.json new file mode 100644 index 00000000..c6ab0e47 --- /dev/null +++ b/slides/test-presentation/slides/manifest.json @@ -0,0 +1,38 @@ +{ + "title": "Test Presentation", + "subtitle": "January 2026", + "createdAt": "2026-01-22T00:00:00.000Z", + "updatedAt": "2026-01-22T00:00:00.000Z", + "slides": [ + { + "id": "slide-001", + "file": "001-introduction.json", + "title": "Introduction", + "layout": "title" + }, + { + "id": "slide-002", + "file": "002-key-metrics.json", + "title": "Key Metrics", + "layout": "stats" + }, + { + "id": "slide-003", + "file": "003-features.json", + "title": "Key Features", + "layout": "list" + }, + { + "id": "slide-004", + "file": "004-comparison.json", + "title": "Comparison", + "layout": "two-column" + }, + { + "id": "slide-005", + "file": "005-quote.json", + "title": "Customer Quote", + "layout": "quote" + } + ] +} diff --git a/slides/test-presentation/styles.css b/slides/test-presentation/styles.css new file mode 100644 index 00000000..e233157c --- /dev/null +++ b/slides/test-presentation/styles.css @@ -0,0 +1,217 @@ +/** + * Slides Presentation Styles + * Color system and utilities adapted from deco.cx design system + */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + width: 100%; + height: 100%; + overflow: hidden; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f0e0d; + color: #faf9f7; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +#root { + width: 100%; + height: 100%; +} + +/* Background colors */ +.bg-dc-950 { background-color: #0f0e0d; } +.bg-dc-900 { background-color: #1a1918; } +.bg-dc-800 { background-color: #2a2927; } +.bg-primary-light { background-color: #c4df1b; } +.bg-purple-light { background-color: #d4a5ff; } +.bg-yellow-light { background-color: #ffd666; } + +/* Accent background colors */ +.bg-accent-green { background-color: #c4df1b; } +.bg-accent-purple { background-color: #d4a5ff; } +.bg-accent-yellow { background-color: #ffd666; } + +/* Text colors */ +.text-dc-50 { color: #faf9f7; } +.text-dc-100 { color: #f0eeeb; } +.text-dc-200 { color: #e1ddd8; } +.text-dc-300 { color: #c9c4bc; } +.text-dc-400 { color: #a39d94; } +.text-dc-500 { color: #6d6a66; } +.text-dc-600 { color: #52504c; } +.text-dc-700 { color: #3a3936; } +.text-dc-800 { color: #2a2927; } +.text-dc-900 { color: #1a1918; } + +/* Accent text colors */ +.text-accent-green { color: #c4df1b; } +.text-accent-purple { color: #d4a5ff; } +.text-accent-yellow { color: #ffd666; } +.text-primary-light { color: #c4df1b; } + +/* Border colors */ +.border-dc-600 { border-color: #52504c; } +.border-dc-700 { border-color: #3a3936; } +.border-dc-800 { border-color: #2a2927; } +.border-primary-light { border-color: #c4df1b; } +.border-primary-light\/30 { border-color: rgba(196, 223, 27, 0.3); } + +/* Layout utilities */ +.fixed { position: fixed; } +.absolute { position: absolute; } +.relative { position: relative; } +.inset-0 { top: 0; right: 0; bottom: 0; left: 0; } + +.flex { display: flex; } +.grid { display: grid; } +.block { display: block; } +.inline-block { display: inline-block; } + +.flex-col { flex-direction: column; } +.flex-1 { flex: 1 1 0%; } +.flex-shrink-0 { flex-shrink: 0; } + +.items-start { align-items: flex-start; } +.items-center { align-items: center; } +.items-end { align-items: flex-end; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.justify-end { justify-content: flex-end; } + +.text-center { text-align: center; } +.text-left { text-align: left; } + +/* Sizing */ +.w-full { width: 100%; } +.h-full { height: 100%; } +.w-screen { width: 100vw; } +.h-screen { height: 100vh; } + +/* Overflow */ +.overflow-hidden { overflow: hidden; } + +/* Cursor */ +.cursor-pointer { cursor: pointer; } +.cursor-not-allowed { cursor: not-allowed; } + +/* Pointer events */ +.pointer-events-auto { pointer-events: auto; } +.pointer-events-none { pointer-events: none; } + +/* Border radius */ +.rounded-full { border-radius: 9999px; } +.rounded-lg { border-radius: 8px; } +.rounded-xl { border-radius: 12px; } + +/* Border */ +.border { border-width: 1px; border-style: solid; } + +/* Transitions */ +.transition-all { transition: all 150ms ease; } +.transition-opacity { transition: opacity 150ms ease; } +.transition-colors { transition: color 150ms ease, background-color 150ms ease, border-color 150ms ease; } +.duration-300 { transition-duration: 300ms; } +.duration-200 { transition-duration: 200ms; } + +/* Opacity */ +.opacity-0 { opacity: 0; } +.opacity-50 { opacity: 0.5; } +.opacity-100 { opacity: 1; } + +/* Z-index */ +.z-50 { z-index: 50; } + +/* Font utilities */ +.font-mono { font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace; } +.uppercase { text-transform: uppercase; } +.tracking-widest { letter-spacing: 0.1em; } +.leading-none { line-height: 1; } +.leading-tight { line-height: 1.25; } +.not-italic { font-style: normal; } + +/* Hover states */ +.hover\:border-dc-600:hover { border-color: #52504c; } +.hover\:text-dc-300:hover { color: #c9c4bc; } +.hover\:bg-primary-light\/20:hover { background-color: rgba(196, 223, 27, 0.2); } +.hover\:opacity-80:hover { opacity: 0.8; } + +/* Alpha backgrounds */ +.bg-primary-light\/10 { background-color: rgba(196, 223, 27, 0.1); } +.bg-primary-light\/20 { background-color: rgba(196, 223, 27, 0.2); } + +/* Button reset */ +button { + background: none; + border: none; + cursor: pointer; + font: inherit; + color: inherit; +} + +button:disabled { + cursor: not-allowed; +} + +/* Blockquote reset */ +blockquote { + margin: 0; + padding: 0; +} + +/* List reset */ +ul, li { + list-style: none; + margin: 0; + padding: 0; +} + +/* Image reset */ +img { + max-width: 100%; + display: block; +} + +/* Selection styling */ +::selection { + background: rgba(196, 223, 27, 0.3); + color: #faf9f7; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #1a1918; +} + +::-webkit-scrollbar-thumb { + background: #3a3936; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #52504c; +} + +/* Fullscreen styling */ +:fullscreen { + background: #0f0e0d; +} + +:-webkit-full-screen { + background: #0f0e0d; +} + +:-moz-full-screen { + background: #0f0e0d; +} diff --git a/slides/tsconfig.json b/slides/tsconfig.json new file mode 100644 index 00000000..66bcf5ae --- /dev/null +++ b/slides/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "verbatimModuleSyntax": false, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "allowJs": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + /* Path Aliases */ + "baseUrl": ".", + "paths": { + "server/*": ["./server/*"] + } + }, + "include": [ + "server" + ] +}