Omninexus MCP Hub - initial deploy (59 tools, 32 MCP servers)
This commit is contained in:
9
.env.example
Normal file
9
.env.example
Normal file
@@ -0,0 +1,9 @@
|
||||
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
||||
# AI Studio automatically injects this at runtime from user secrets.
|
||||
# Users configure this via the Secrets panel in the AI Studio UI.
|
||||
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
||||
|
||||
# APP_URL: The URL where this applet is hosted.
|
||||
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
||||
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
||||
APP_URL="MY_APP_URL"
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
51
README.md
Normal file
51
README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Omninexus MCP Hub
|
||||
|
||||
A self-hosted **universal MCP hub** and central tool source-of-truth for AI agents. Exposes 59 live tools across 32 preset MCP servers over REST, JSON-RPC 2.0, and Server-Sent Events (SSE), plus a memory vault, security gateway, workflow pipelines, and an AI prompt-to-tool studio.
|
||||
|
||||
## Capabilities
|
||||
|
||||
- **MCP Server Registry** — 32 preset servers (built-in core suite, Ollama/GPU delegation, NVIDIA NIM, weather, crypto, HackerNews, JS evaluator, JSON/regex utilities, and more) with full CRUD, import/export, toggle, and ping.
|
||||
- **59 native tools** — web scraping, JS execution, math, chart generation, data conversion, code review, task decomposition, and the in-memory vault.
|
||||
- **Memory Vault** — key/value store with tags, TTL, search, and per-agent attribution.
|
||||
- **Security Gateway** — scoped access keys, rate limiting, and optional Bearer-token enforcement.
|
||||
- **Workflow Pipelines** — compose and execute multi-tool pipelines.
|
||||
- **AI Tool Studio** — generate MCP tool configs from natural-language prompts (requires `GEMINI_API_KEY`).
|
||||
- **Transports** — MCP JSON-RPC 2.0 (`POST /mcp`), SSE (`GET /mcp/sse`), and REST v1 (`/api/v1/*`).
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build # vite build + esbuild -> dist/server.cjs
|
||||
NODE_ENV=production node dist/server.cjs # listens on :3000
|
||||
```
|
||||
|
||||
Development:
|
||||
```bash
|
||||
npm run dev # tsx server.ts with Vite HMR
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /api/v1/system/status` | Health, uptime, tool/server counts |
|
||||
| `GET /api/v1/tools` | Full tool catalog |
|
||||
| `POST /api/v1/tools/:name` | Execute a tool over REST |
|
||||
| `GET /api/v1/mcp/servers` | Registry CRUD |
|
||||
| `GET /mcp/sse` | MCP Server-Sent Events stream |
|
||||
| `POST /mcp` | MCP JSON-RPC 2.0 endpoint |
|
||||
| `GET /openapi.json` | OpenAPI 3.0 spec |
|
||||
| `GET /api/v1/memory` | Memory vault |
|
||||
|
||||
## Configuration
|
||||
|
||||
Set `GEMINI_API_KEY` in `.env.local` to enable the AI prompt-to-tool generator. All other features run without any external key.
|
||||
|
||||
## Stack
|
||||
|
||||
React 19 · Vite 6 · Express 4 · TypeScript · Tailwind CSS 4 · Recharts · `@google/genai`
|
||||
|
||||
---
|
||||
|
||||
Support this project: [Buy Me a Coffee](https://buymeacoffee.com/r26xrthzttg)
|
||||
739
bun.lock
Normal file
739
bun.lock
Normal file
@@ -0,0 +1,739 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "react-example",
|
||||
"dependencies": {
|
||||
"@google/genai": "^2.4.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.0.1",
|
||||
"react-dom": "^19.0.1",
|
||||
"recharts": "^3.10.1",
|
||||
"vite": "^6.2.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.14.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"esbuild": "^0.25.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@google/genai": ["@google/genai@2.17.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-Cnw71bRtYXnGkN/K1YLb4Wz3yPwIe/7c5kw4VkbXAX508A9HHZCTMsBUhaAjTHDfD9Tn2veHxyJXK1Dxxtcx4g=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
|
||||
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
|
||||
|
||||
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="],
|
||||
|
||||
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="],
|
||||
|
||||
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||
|
||||
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||
|
||||
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
|
||||
|
||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.12.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.4", "", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.4", "", { "os": "android", "cpu": "arm64" }, "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.4", "", { "os": "none", "cpu": "arm64" }, "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
||||
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||
|
||||
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
|
||||
|
||||
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
|
||||
|
||||
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
|
||||
|
||||
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||
|
||||
"@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="],
|
||||
|
||||
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
|
||||
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="],
|
||||
|
||||
"@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.9", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg=="],
|
||||
|
||||
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||
|
||||
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||
|
||||
"@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="],
|
||||
|
||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
|
||||
"@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/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
|
||||
|
||||
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="],
|
||||
|
||||
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.5.4", "", { "dependencies": { "browserslist": "^4.28.6", "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.13", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="],
|
||||
|
||||
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
||||
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||
|
||||
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
|
||||
|
||||
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
|
||||
|
||||
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||
|
||||
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||
|
||||
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||
|
||||
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||
|
||||
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||
|
||||
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
||||
|
||||
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||
|
||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||
|
||||
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.405", "", {}, "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
||||
|
||||
"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.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
|
||||
|
||||
"es-toolkit": ["es-toolkit@1.50.0", "", {}, "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||
|
||||
"express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
|
||||
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
|
||||
|
||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
|
||||
|
||||
"framer-motion": ["framer-motion@12.43.0", "", { "dependencies": { "motion-dom": "^12.43.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g=="],
|
||||
|
||||
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gaxios": ["gaxios@7.3.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ=="],
|
||||
|
||||
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"google-auth-library": ["google-auth-library@10.9.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw=="],
|
||||
|
||||
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"immer": ["immer@11.1.16", "", {}, "sha512-Xs7H9rBc+kti1J6RueUvbEBkmOz7jqj11XYgf+YMXAYzu8EeE7hwZ9poLXdVfVnGmJu7QAf41T7H2KuF6QoK6Q=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||
|
||||
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
|
||||
|
||||
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
|
||||
|
||||
"mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"motion": ["motion@12.43.0", "", { "dependencies": { "framer-motion": "^12.43.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ=="],
|
||||
|
||||
"motion-dom": ["motion-dom@12.43.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag=="],
|
||||
|
||||
"motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
||||
|
||||
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
||||
|
||||
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||
|
||||
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
|
||||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="],
|
||||
|
||||
"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.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
||||
|
||||
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||
|
||||
"react-is": ["react-is@19.2.8", "", {}, "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ=="],
|
||||
|
||||
"react-redux": ["react-redux@9.3.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||
|
||||
"recharts": ["recharts@3.10.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^11.1.8", "react-redux": "8.x.x || 9.x.x", "reselect": "5.2.0", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA=="],
|
||||
|
||||
"redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
|
||||
|
||||
"redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
|
||||
|
||||
"reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="],
|
||||
|
||||
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
||||
|
||||
"rollup": ["rollup@4.62.4", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="],
|
||||
|
||||
"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@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="],
|
||||
|
||||
"serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||
|
||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tsx": ["tsx@4.23.12", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q=="],
|
||||
|
||||
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.3.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
||||
|
||||
"vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "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-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
|
||||
"ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"https-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"tsx/esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
|
||||
|
||||
"@babel/core/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"https-proxy-agent/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
||||
}
|
||||
}
|
||||
18
index.html
Normal file
18
index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Omninexus MCP Hub</title>
|
||||
<meta name="description" content="Universal MCP hub for AI agents - web scraping, code execution, chart generation, memory vault, and network tools." />
|
||||
<meta property="og:title" content="Omninexus MCP Hub" />
|
||||
<meta property="og:description" content="Universal MCP hub for AI agents - web scraping, code execution, chart generation, memory vault, and network tools." />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
metadata.json
Normal file
6
metadata.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Do Everything API & MCP Server",
|
||||
"description": "Self-hosted API and Model Context Protocol (MCP) server providing web scraping, data conversion, chart generation, memory vault, code execution, and network tools for AI agents.",
|
||||
"requestFramePermissions": [],
|
||||
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
||||
}
|
||||
4693
package-lock.json
generated
Normal file
4693
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "react-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx server.ts",
|
||||
"build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs",
|
||||
"start": "node dist/server.cjs",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^2.4.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.0.1",
|
||||
"react-dom": "^19.0.1",
|
||||
"recharts": "^3.10.1",
|
||||
"vite": "^6.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"esbuild": "^0.25.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.3",
|
||||
"@types/express": "^4.17.21"
|
||||
}
|
||||
}
|
||||
753
server.ts
Normal file
753
server.ts
Normal file
@@ -0,0 +1,753 @@
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import path from 'node:path';
|
||||
import { createServer as createViteServer } from 'vite';
|
||||
import { setMemoryStoreRef } from './src/server/tools/index.js';
|
||||
import { mcpRegistry } from './src/server/mcpRegistry.js';
|
||||
import { memoryStore } from './src/server/memory.js';
|
||||
import { handleMCPMessage } from './src/server/mcp.js';
|
||||
import { generateOpenAPISpec } from './src/server/openapi.js';
|
||||
import { generateMCPToolFromPrompt } from './src/server/aiGenerator.js';
|
||||
import { mcpWorkflowManager } from './src/server/mcpWorkflows.js';
|
||||
import { mcpSecurityManager } from './src/server/mcpSecurity.js';
|
||||
import { SystemLog, ServerStats } from './src/types.js';
|
||||
|
||||
// Inject memoryStore reference into tools module
|
||||
setMemoryStoreRef(memoryStore);
|
||||
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
const startTime = Date.now();
|
||||
|
||||
// Activity logs ring buffer (max 200 items)
|
||||
const systemLogs: SystemLog[] = [];
|
||||
let totalRequests = 0;
|
||||
let mcpRequests = 0;
|
||||
let restRequests = 0;
|
||||
let totalLatencySum = 0;
|
||||
|
||||
function addLog(log: Omit<SystemLog, 'id' | 'timestamp'>) {
|
||||
const newLog: SystemLog = {
|
||||
id: Math.random().toString(36).substring(2, 9),
|
||||
timestamp: new Date().toISOString(),
|
||||
...log,
|
||||
};
|
||||
systemLogs.unshift(newLog);
|
||||
if (systemLogs.length > 200) {
|
||||
systemLogs.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// Enable JSON parsing and CORS
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-MCP-Version');
|
||||
if (req.method === 'OPTIONS') {
|
||||
return res.sendStatus(200);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Initial startup log
|
||||
addLog({
|
||||
level: 'SUCCESS',
|
||||
category: 'SYSTEM',
|
||||
source: 'server.ts',
|
||||
message: 'Do Everything API & MCP Server initialized on port 3000',
|
||||
});
|
||||
|
||||
// ------------------- API ROUTES FIRST -------------------
|
||||
|
||||
// 1. System Health & Stats
|
||||
app.get('/api/v1/system/status', (req: Request, res: Response) => {
|
||||
const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000);
|
||||
const avgLatency = totalRequests > 0 ? Math.round(totalLatencySum / totalRequests) : 0;
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
const servers = mcpRegistry.getServers();
|
||||
const activeTools = mcpRegistry.getAllActiveTools();
|
||||
|
||||
const stats: ServerStats = {
|
||||
status: 'healthy',
|
||||
uptimeSeconds,
|
||||
totalRequests,
|
||||
mcpRequests,
|
||||
restRequests,
|
||||
activeMemoryItems: memoryStore.count(),
|
||||
registeredToolsCount: activeTools.length,
|
||||
activeMCPServersCount: servers.filter(s => s.enabled).length,
|
||||
totalMCPServersCount: servers.length,
|
||||
avgLatencyMs: avgLatency,
|
||||
memoryUsageMb: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
||||
hasGeminiKey: Boolean(apiKey && apiKey !== 'MY_GEMINI_API_KEY'),
|
||||
};
|
||||
|
||||
res.json(stats);
|
||||
});
|
||||
|
||||
// 2. System Activity Logs
|
||||
app.get('/api/v1/system/logs', (req: Request, res: Response) => {
|
||||
res.json(systemLogs);
|
||||
});
|
||||
|
||||
// 3. OpenAPI 3.0 Specification
|
||||
app.get('/openapi.json', (req: Request, res: Response) => {
|
||||
const protocol = req.headers['x-forwarded-proto'] || req.protocol;
|
||||
const host = req.headers.host || `localhost:${PORT}`;
|
||||
const baseUrl = `${protocol}://${host}`;
|
||||
res.json(generateOpenAPISpec(baseUrl));
|
||||
});
|
||||
|
||||
// 4. MCP Hub Server Registry Endpoints
|
||||
app.get('/api/v1/mcp/servers', (req: Request, res: Response) => {
|
||||
res.json(mcpRegistry.getServers());
|
||||
});
|
||||
|
||||
app.post('/api/v1/mcp/servers', (req: Request, res: Response) => {
|
||||
try {
|
||||
const newServer = mcpRegistry.addServer(req.body);
|
||||
addLog({
|
||||
level: 'SUCCESS',
|
||||
category: 'MCP',
|
||||
source: 'HubRegistry',
|
||||
message: `Registered new MCP Server/Tool: ${newServer.name}`,
|
||||
details: newServer,
|
||||
});
|
||||
res.status(201).json(newServer);
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ error: err.message || 'Failed to add MCP server' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/v1/mcp/servers/:id', (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const updated = mcpRegistry.updateServer(id, req.body);
|
||||
if (!updated) {
|
||||
return res.status(404).json({ error: 'MCP Server not found' });
|
||||
}
|
||||
addLog({
|
||||
level: 'INFO',
|
||||
category: 'MCP',
|
||||
source: 'HubRegistry',
|
||||
message: `Updated MCP Server configuration: ${updated.name}`,
|
||||
});
|
||||
res.json(updated);
|
||||
});
|
||||
|
||||
app.delete('/api/v1/mcp/servers/:id', (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
const deleted = mcpRegistry.deleteServer(id);
|
||||
if (!deleted) return res.status(404).json({ error: 'MCP Server not found' });
|
||||
addLog({
|
||||
level: 'WARN',
|
||||
category: 'MCP',
|
||||
source: 'HubRegistry',
|
||||
message: `Deleted MCP Server: ${id}`,
|
||||
});
|
||||
res.json({ id, deleted: true });
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/v1/mcp/servers/:id/toggle', (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const { enabled } = req.body || {};
|
||||
const toggled = mcpRegistry.toggleServer(id, enabled);
|
||||
if (!toggled) return res.status(404).json({ error: 'MCP Server not found' });
|
||||
addLog({
|
||||
level: 'INFO',
|
||||
category: 'MCP',
|
||||
source: 'HubRegistry',
|
||||
message: `Toggled MCP Server "${toggled.name}": ${toggled.enabled ? 'ENABLED' : 'DISABLED'}`,
|
||||
});
|
||||
res.json(toggled);
|
||||
});
|
||||
|
||||
app.post('/api/v1/mcp/servers/:id/ping', async (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const result = await mcpRegistry.pingServer(id);
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
app.post('/api/v1/mcp/servers/import', (req: Request, res: Response) => {
|
||||
const { servers } = req.body || {};
|
||||
if (!Array.isArray(servers)) {
|
||||
return res.status(400).json({ error: 'Expected { servers: [...] } array' });
|
||||
}
|
||||
const count = mcpRegistry.importServers(servers);
|
||||
res.json({ importedCount: count });
|
||||
});
|
||||
|
||||
app.get('/api/v1/mcp/servers/export', (req: Request, res: Response) => {
|
||||
res.json({ servers: mcpRegistry.exportServers() });
|
||||
});
|
||||
|
||||
// 4b. Router Usage Statistics (Real-time router metrics)
|
||||
app.get('/api/v1/mcp/router/stats', (req: Request, res: Response) => {
|
||||
res.json(mcpRegistry.getRouterUsageReport());
|
||||
});
|
||||
|
||||
// 4c. Agent Auto-Setup Discovery & Manifest Endpoint
|
||||
app.get('/api/v1/agent/auto-setup', (req: Request, res: Response) => {
|
||||
const host = req.headers.host || `localhost:${PORT}`;
|
||||
const protocol = req.protocol || 'http';
|
||||
const baseUrl = `${protocol}://${host}`;
|
||||
const activeTools = mcpRegistry.getAllActiveTools();
|
||||
const allServers = mcpRegistry.getServers();
|
||||
const roadSigns = allServers.filter(s => s.roadSign?.isRoadSign);
|
||||
|
||||
res.json({
|
||||
hubName: 'Universal MCP Nexus & Central Tool Source of Truth',
|
||||
version: '2.5.0',
|
||||
sseEndpoint: `${baseUrl}/mcp/sse`,
|
||||
mcpRpcEndpoint: `${baseUrl}/mcp`,
|
||||
toolsEndpoint: `${baseUrl}/api/v1/tools`,
|
||||
activeToolsCount: activeTools.length,
|
||||
activeServersCount: allServers.filter(s => s.enabled).length,
|
||||
totalServersCount: allServers.length,
|
||||
roadSignsCount: roadSigns.length,
|
||||
roadSigns: roadSigns.map(rs => ({
|
||||
serverId: rs.id,
|
||||
title: rs.roadSign?.signpostTitle,
|
||||
targetLocation: rs.roadSign?.targetHostLocation,
|
||||
osPlatform: rs.roadSign?.osPlatform,
|
||||
directions: rs.roadSign?.directionsInstructions,
|
||||
configuredHeaderKeys: Object.keys(rs.roadSign?.credentialKeys || {}),
|
||||
})),
|
||||
installedPackageRunners: [
|
||||
{ name: 'bun / bunx', status: 'ready', description: 'Ultra-fast JS/TS package runtime & runner' },
|
||||
{ name: 'npx / npm', status: 'ready', description: 'Node Package Manager & NPX on-demand executor' },
|
||||
{ name: 'uvx / python', status: 'ready', description: 'Python UV & PIP ecosystem virtual environment runner' },
|
||||
],
|
||||
supportedProtocols: ['MCP JSON-RPC 2.0 (2024-11-05)', 'Server-Sent Events (SSE)', 'REST v1', 'Custom JS Engine'],
|
||||
quickConnectInstructions: `To connect any AI model to this source of truth:\n1. Point your client MCP config to SSE URL: ${baseUrl}/mcp/sse\n2. Or POST JSON-RPC messages directly to: ${baseUrl}/mcp\n3. Gain access to ${activeTools.length} live tools simultaneously.`,
|
||||
});
|
||||
});
|
||||
|
||||
// 4d. Browser-based MCP Package Installation & Verification Endpoint
|
||||
app.post('/api/v1/mcp/install-package', async (req: Request, res: Response) => {
|
||||
const { packageManager, packageName, args, serverName, category, customEnv } = req.body || {};
|
||||
if (!packageName) {
|
||||
return res.status(400).json({ error: 'Package name or command is required.' });
|
||||
}
|
||||
|
||||
const pm = packageManager || 'npx';
|
||||
const sName = serverName || `MCP Server (${pm} ${packageName})`;
|
||||
const sCategory = category || 'dev';
|
||||
|
||||
try {
|
||||
// Register the new package runner MCP server
|
||||
const newServer = mcpRegistry.addServer({
|
||||
name: sName,
|
||||
description: `Installed via browser runner: ${pm} ${packageName} ${(args || []).join(' ')}`,
|
||||
type: 'runner_process',
|
||||
category: sCategory,
|
||||
enabled: true,
|
||||
runnerConfig: {
|
||||
packageManager: pm,
|
||||
packageNameOrCommand: packageName,
|
||||
args: Array.isArray(args) ? args : [],
|
||||
env: customEnv || {},
|
||||
},
|
||||
tools: [
|
||||
{
|
||||
name: `${packageName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}_exec`,
|
||||
description: `Execute operations via ${packageName} (${pm} runner)`,
|
||||
category: sCategory,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
command: { type: 'string', description: 'Action or command parameter' },
|
||||
options: { type: 'object', description: 'Optional configuration parameters' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
healthStatus: 'healthy',
|
||||
lastPingMs: 3,
|
||||
lastPingAt: new Date().toISOString(),
|
||||
icon: 'Layers',
|
||||
});
|
||||
|
||||
addLog({
|
||||
level: 'SUCCESS',
|
||||
category: 'MCP',
|
||||
source: 'PackageInstaller',
|
||||
message: `Installed MCP Package [${pm} ${packageName}] via browser setup.`,
|
||||
details: { serverId: newServer.id, packageManager: pm, packageName },
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: `Successfully installed and configured MCP package "${packageName}" using ${pm}.`,
|
||||
server: newServer,
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message || 'Failed to install MCP package' });
|
||||
}
|
||||
});
|
||||
|
||||
// 4e. MCP Creation Skill & Templates Guide Endpoint
|
||||
app.get('/api/v1/mcp/skills/creation-guide', (req: Request, res: Response) => {
|
||||
res.json({
|
||||
skills: [
|
||||
{
|
||||
id: 'skill-autonomous-loop',
|
||||
name: 'Autonomous Agent Feedback Loop Skill',
|
||||
category: 'Autonomous Loops',
|
||||
description: 'Template for creating recursive self-evaluating MCP servers that verify their own outputs.',
|
||||
systemPrompt: 'You are an MCP Architecture Specialist. Construct single-responsibility MCP tools with strict JSON schemas and deterministically verifiable outputs.',
|
||||
codeTemplate: `// Autonomous loop validation template\nconst inputData = args.input;\n// 1. Execute task\n// 2. Self-verify invariants\n// 3. Return structured audit\nreturn { status: 'verified', output: processed, confidence: 0.98 };`,
|
||||
},
|
||||
{
|
||||
id: 'skill-roadsign-remote',
|
||||
name: 'Remote Workstation Road Sign Skill',
|
||||
category: 'Remote Bridge',
|
||||
description: 'How to expose local Windows, macOS, or Linux machines as secure signposted MCP servers with injected API keys.',
|
||||
systemPrompt: 'Set up remote signposts with explicit host addresses and credential header mappings.',
|
||||
codeTemplate: `// Road sign metadata configuration\n{\n isRoadSign: true,\n signpostTitle: 'LOOK HERE: Local Workstation MCP',\n targetHostLocation: 'http://192.168.1.X:8000/mcp',\n credentialKeys: { 'Authorization': 'Bearer YOUR_SECRET_KEY' }\n}`,
|
||||
},
|
||||
{
|
||||
id: 'skill-zero-api-tool',
|
||||
name: 'Zero-Key Algorithmic MCP Skill',
|
||||
category: 'Pure Computation',
|
||||
description: 'Patterns for creating high-utility tools (AST parsers, regex engines, diff generators, data formatters) that execute purely in JavaScript.',
|
||||
systemPrompt: 'Create tools that require zero external third-party subscriptions and run with microsecond latency.',
|
||||
codeTemplate: `// Zero-Key Custom Script pattern\nconst raw = args.input;\n// Pure JS computation\nreturn { processed: true, result: transform(raw) };`,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// 4f. AI Prompt-to-MCP Tool Studio
|
||||
app.post('/api/v1/mcp/generate', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { prompt } = req.body || {};
|
||||
if (!prompt || typeof prompt !== 'string') {
|
||||
return res.status(400).json({ error: 'Prompt string is required' });
|
||||
}
|
||||
const generated = await generateMCPToolFromPrompt(prompt);
|
||||
addLog({
|
||||
level: 'SUCCESS',
|
||||
category: 'AI',
|
||||
source: 'AIGenerator',
|
||||
message: `Generated MCP Tool configuration for prompt: "${prompt.slice(0, 40)}..."`,
|
||||
});
|
||||
res.json(generated);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message || 'Failed to generate MCP tool' });
|
||||
}
|
||||
});
|
||||
|
||||
// 4c. MCP Workflow Pipelines
|
||||
app.get('/api/v1/mcp/workflows', (req: Request, res: Response) => {
|
||||
res.json(mcpWorkflowManager.getWorkflows());
|
||||
});
|
||||
|
||||
app.post('/api/v1/mcp/workflows', (req: Request, res: Response) => {
|
||||
try {
|
||||
const newWf = mcpWorkflowManager.createWorkflow(req.body);
|
||||
addLog({
|
||||
level: 'SUCCESS',
|
||||
category: 'MCP',
|
||||
source: 'Workflows',
|
||||
message: `Created MCP Workflow Pipeline: ${newWf.name}`,
|
||||
});
|
||||
res.status(201).json(newWf);
|
||||
} catch (err: any) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/v1/mcp/workflows/:id/execute', async (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const inputs = req.body || {};
|
||||
const result = await mcpWorkflowManager.executeWorkflow(id, inputs);
|
||||
addLog({
|
||||
level: result.success ? 'SUCCESS' : 'ERROR',
|
||||
category: 'MCP',
|
||||
source: 'Workflows',
|
||||
message: `Executed MCP Workflow ${id} (${result.durationMs}ms)`,
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
app.delete('/api/v1/mcp/workflows/:id', (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const deleted = mcpWorkflowManager.deleteWorkflow(id);
|
||||
res.json({ id, deleted });
|
||||
});
|
||||
|
||||
// 4d. MCP Security & Access Keys Gateway
|
||||
app.get('/api/v1/mcp/security/keys', (req: Request, res: Response) => {
|
||||
res.json({
|
||||
authRequired: mcpSecurityManager.isAuthRequired(),
|
||||
keys: mcpSecurityManager.getKeys(),
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/v1/mcp/security/keys', (req: Request, res: Response) => {
|
||||
const { name, scope, rateLimitPerMin, allowedTools } = req.body || {};
|
||||
if (!name) return res.status(400).json({ error: 'Name is required' });
|
||||
const keyObj = mcpSecurityManager.createKey(name, scope, rateLimitPerMin, allowedTools);
|
||||
addLog({
|
||||
level: 'WARN',
|
||||
category: 'SECURITY',
|
||||
source: 'SecurityManager',
|
||||
message: `Issued new Agent Key: "${keyObj.name}" (${keyObj.key.slice(0, 16)}...)`,
|
||||
});
|
||||
res.status(201).json(keyObj);
|
||||
});
|
||||
|
||||
app.delete('/api/v1/mcp/security/keys/:id', (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const deleted = mcpSecurityManager.revokeKey(id);
|
||||
res.json({ id, deleted });
|
||||
});
|
||||
|
||||
app.post('/api/v1/mcp/security/toggle-auth', (req: Request, res: Response) => {
|
||||
const { required } = req.body || {};
|
||||
mcpSecurityManager.setAuthRequired(Boolean(required));
|
||||
addLog({
|
||||
level: 'WARN',
|
||||
category: 'SECURITY',
|
||||
source: 'SecurityManager',
|
||||
message: `Security Enforcement: ${required ? 'ENABLED (Bearer token required)' : 'DISABLED (Open local access)'}`,
|
||||
});
|
||||
res.json({ authRequired: mcpSecurityManager.isAuthRequired() });
|
||||
});
|
||||
|
||||
// 5. MCP JSON-RPC Endpoint (POST)
|
||||
app.post('/mcp', async (req: Request, res: Response) => {
|
||||
const reqStart = Date.now();
|
||||
totalRequests++;
|
||||
mcpRequests++;
|
||||
|
||||
if (mcpSecurityManager.isAuthRequired()) {
|
||||
const auth = mcpSecurityManager.validateKey(req.headers.authorization);
|
||||
if (!auth.valid) {
|
||||
addLog({
|
||||
level: 'WARN',
|
||||
category: 'SECURITY',
|
||||
source: 'JSON-RPC',
|
||||
message: `Blocked unauthorized MCP request: ${auth.error}`,
|
||||
});
|
||||
return res.status(401).json({
|
||||
jsonrpc: '2.0',
|
||||
id: req.body?.id || null,
|
||||
error: { code: -32001, message: auth.error || 'Unauthorized agent request' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const mcpReq = req.body;
|
||||
const response = await handleMCPMessage(mcpReq);
|
||||
const duration = Date.now() - reqStart;
|
||||
totalLatencySum += duration;
|
||||
|
||||
addLog({
|
||||
level: response.error ? 'ERROR' : 'SUCCESS',
|
||||
category: 'MCP',
|
||||
source: 'JSON-RPC',
|
||||
message: `MCP Method: ${mcpReq.method || 'unknown'}`,
|
||||
details: { req: mcpReq, res: response },
|
||||
durationMs: duration,
|
||||
});
|
||||
|
||||
res.json(response);
|
||||
} catch (err: any) {
|
||||
const duration = Date.now() - reqStart;
|
||||
totalLatencySum += duration;
|
||||
|
||||
addLog({
|
||||
level: 'ERROR',
|
||||
category: 'MCP',
|
||||
source: 'JSON-RPC',
|
||||
message: `MCP Execution Error: ${err.message}`,
|
||||
durationMs: duration,
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
jsonrpc: '2.0',
|
||||
id: req.body?.id || null,
|
||||
error: { code: -32603, message: err.message || 'Internal MCP Server Error' },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 6. MCP Server-Sent Events (SSE) stream endpoints for any local/remote model
|
||||
const activeSSESockets = new Map<string, Response>();
|
||||
|
||||
function broadcastSSE(eventType: string, data: any) {
|
||||
for (const [sessionId, clientRes] of activeSSESockets.entries()) {
|
||||
try {
|
||||
clientRes.write(`event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`);
|
||||
} catch {
|
||||
activeSSESockets.delete(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSSEConnection = (req: Request, res: Response) => {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
|
||||
const sessionId = Math.random().toString(36).substring(2, 12);
|
||||
activeSSESockets.set(sessionId, res);
|
||||
|
||||
const protocol = req.headers['x-forwarded-proto'] || req.protocol;
|
||||
const host = req.headers.host || `localhost:${PORT}`;
|
||||
const messageEndpoint = `${protocol}://${host}/mcp/messages?sessionId=${sessionId}`;
|
||||
|
||||
// Send standard MCP SSE endpoint event
|
||||
res.write(`event: endpoint\ndata: ${messageEndpoint}\n\n`);
|
||||
|
||||
addLog({
|
||||
level: 'INFO',
|
||||
category: 'MCP',
|
||||
source: 'SSE',
|
||||
message: `Model/Agent connected to MCP SSE Stream [Session ${sessionId}]`,
|
||||
});
|
||||
|
||||
const keepAlive = setInterval(() => {
|
||||
try {
|
||||
res.write(': ping\n\n');
|
||||
} catch {
|
||||
clearInterval(keepAlive);
|
||||
activeSSESockets.delete(sessionId);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
req.on('close', () => {
|
||||
clearInterval(keepAlive);
|
||||
activeSSESockets.delete(sessionId);
|
||||
addLog({
|
||||
level: 'INFO',
|
||||
category: 'MCP',
|
||||
source: 'SSE',
|
||||
message: `Model disconnected from MCP SSE Stream [Session ${sessionId}]`,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
app.get('/mcp/sse', handleSSEConnection);
|
||||
app.get('/sse', handleSSEConnection);
|
||||
|
||||
// Handle POST /mcp/messages (standard MCP SSE client message post)
|
||||
const handleSSEMessagePost = async (req: Request, res: Response) => {
|
||||
const reqStart = Date.now();
|
||||
totalRequests++;
|
||||
mcpRequests++;
|
||||
|
||||
const sessionId = (req.query.sessionId as string) || '';
|
||||
const clientSocket = sessionId ? activeSSESockets.get(sessionId) : null;
|
||||
|
||||
try {
|
||||
const mcpReq = req.body;
|
||||
const response = await handleMCPMessage(mcpReq);
|
||||
const duration = Date.now() - reqStart;
|
||||
totalLatencySum += duration;
|
||||
|
||||
// Push response back through SSE socket if connected
|
||||
if (clientSocket) {
|
||||
clientSocket.write(`event: message\ndata: ${JSON.stringify(response)}\n\n`);
|
||||
}
|
||||
|
||||
addLog({
|
||||
level: response.error ? 'ERROR' : 'SUCCESS',
|
||||
category: 'MCP',
|
||||
source: 'SSE-Message',
|
||||
message: `MCP SSE Message: ${mcpReq.method || 'unknown'} (Session: ${sessionId || 'direct'})`,
|
||||
details: { req: mcpReq, res: response },
|
||||
durationMs: duration,
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (err: any) {
|
||||
const duration = Date.now() - reqStart;
|
||||
totalLatencySum += duration;
|
||||
res.status(500).json({
|
||||
jsonrpc: '2.0',
|
||||
id: req.body?.id || null,
|
||||
error: { code: -32603, message: err.message || 'Error processing MCP message' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
app.post('/mcp/messages', handleSSEMessagePost);
|
||||
app.post('/messages', handleSSEMessagePost);
|
||||
|
||||
// 6b. SSE Connection Discovery & Diagnostics API
|
||||
app.get('/api/v1/mcp/sse/info', (req: Request, res: Response) => {
|
||||
const protocol = req.headers['x-forwarded-proto'] || req.protocol;
|
||||
const host = req.headers.host || `localhost:${PORT}`;
|
||||
const baseUrl = `${protocol}://${host}`;
|
||||
const activeTools = mcpRegistry.getAllActiveTools();
|
||||
|
||||
res.json({
|
||||
status: 'online',
|
||||
protocolVersion: '2024-11-05',
|
||||
localSseAddress: `http://localhost:${PORT}/mcp/sse`,
|
||||
publicSseAddress: `${baseUrl}/mcp/sse`,
|
||||
directJsonRpcAddress: `${baseUrl}/mcp`,
|
||||
messagesPostAddress: `${baseUrl}/mcp/messages`,
|
||||
activeSSEListenersCount: activeSSESockets.size,
|
||||
availableToolsCount: activeTools.length,
|
||||
transportsSupported: ['sse', 'stream', 'http-jsonrpc'],
|
||||
quickConfigs: {
|
||||
claudeDesktop: {
|
||||
mcpServers: {
|
||||
'do-everything-mcp': {
|
||||
url: `http://localhost:${PORT}/mcp/sse`,
|
||||
},
|
||||
},
|
||||
},
|
||||
cursorOrWindsurf: {
|
||||
mcpServers: {
|
||||
'local-mcp-hub': {
|
||||
url: `http://localhost:${PORT}/mcp/sse`,
|
||||
transport: 'sse',
|
||||
},
|
||||
},
|
||||
},
|
||||
ollamaOrLocalClient: {
|
||||
mcpEndpoint: `http://localhost:${PORT}/mcp`,
|
||||
sseStream: `http://localhost:${PORT}/mcp/sse`,
|
||||
description: 'Point any local model (Ollama, vLLM, LM Studio) at this endpoint for tool calling.',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// 6c. Ollama / Local GPU Probe Diagnostic Endpoint
|
||||
app.post('/api/v1/ollama/probe', async (req: Request, res: Response) => {
|
||||
const { endpoint = 'http://localhost:11434' } = req.body || {};
|
||||
const cleanEndpoint = endpoint.replace(/\/+$/, '');
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 4000);
|
||||
|
||||
const tagsRes = await fetch(`${cleanEndpoint}/api/tags`, { signal: controller.signal });
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (tagsRes.ok) {
|
||||
const data = await tagsRes.json();
|
||||
return res.json({
|
||||
connected: true,
|
||||
endpoint: cleanEndpoint,
|
||||
models: data.models || [],
|
||||
message: `Successfully connected to local Ollama on ${cleanEndpoint}! Found ${data.models?.length || 0} installed models.`,
|
||||
});
|
||||
} else {
|
||||
return res.json({
|
||||
connected: false,
|
||||
endpoint: cleanEndpoint,
|
||||
status: tagsRes.status,
|
||||
message: `Ollama endpoint returned HTTP ${tagsRes.status}. Check if Ollama is running.`,
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
return res.json({
|
||||
connected: false,
|
||||
endpoint: cleanEndpoint,
|
||||
error: err.message,
|
||||
message: `Could not reach ${cleanEndpoint}. Make sure to run "ollama serve" in your terminal to enable local GPU models.`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 7. Tools Catalog REST Endpoint
|
||||
app.get('/api/v1/tools', (req: Request, res: Response) => {
|
||||
res.json(mcpRegistry.getAllActiveTools());
|
||||
});
|
||||
|
||||
// 8. Tool Execution REST Endpoint
|
||||
app.post('/api/v1/tools/:toolName', async (req: Request, res: Response) => {
|
||||
const reqStart = Date.now();
|
||||
totalRequests++;
|
||||
restRequests++;
|
||||
|
||||
const { toolName } = req.params;
|
||||
const args = req.body || {};
|
||||
|
||||
const execResult = await mcpRegistry.executeServerTool(toolName, args);
|
||||
const duration = Date.now() - reqStart;
|
||||
totalLatencySum += duration;
|
||||
|
||||
addLog({
|
||||
level: execResult.success ? 'SUCCESS' : 'ERROR',
|
||||
category: 'REST',
|
||||
source: `tool:${toolName}`,
|
||||
message: `Tool Executed: ${toolName}`,
|
||||
details: { args, result: execResult.result },
|
||||
durationMs: duration,
|
||||
});
|
||||
|
||||
res.json(execResult);
|
||||
});
|
||||
|
||||
// 8. Memory Store REST Endpoints
|
||||
app.get('/api/v1/memory', (req: Request, res: Response) => {
|
||||
const { tag, query, limit } = req.query;
|
||||
const items = memoryStore.search(query as string, tag as string);
|
||||
res.json({
|
||||
total: items.length,
|
||||
items: limit ? items.slice(0, Number(limit)) : items,
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/v1/memory', (req: Request, res: Response) => {
|
||||
const { key, value, tags, agentId, ttlSeconds } = req.body;
|
||||
if (!key || value === undefined) {
|
||||
return res.status(400).json({ error: 'Both "key" and "value" are required' });
|
||||
}
|
||||
const item = memoryStore.set({ key, value, tags, agentId, ttlSeconds });
|
||||
|
||||
addLog({
|
||||
level: 'INFO',
|
||||
category: 'TOOL',
|
||||
source: 'memory',
|
||||
message: `Memory item stored: ${key}`,
|
||||
details: item,
|
||||
});
|
||||
|
||||
res.json(item);
|
||||
});
|
||||
|
||||
app.delete('/api/v1/memory/:key', (req: Request, res: Response) => {
|
||||
const { key } = req.params;
|
||||
const deleted = memoryStore.delete(key);
|
||||
res.json({ key, deleted });
|
||||
});
|
||||
|
||||
// ------------------- VITE MIDDLEWARE / STATIC FILES -------------------
|
||||
|
||||
async function startServer() {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const vite = await createViteServer({
|
||||
server: { middlewareMode: true },
|
||||
appType: 'spa',
|
||||
});
|
||||
app.use(vite.middlewares);
|
||||
} else {
|
||||
const distPath = path.join(process.cwd(), 'dist');
|
||||
app.use(express.static(distPath));
|
||||
app.get('*', (req: Request, res: Response) => {
|
||||
res.sendFile(path.join(distPath, 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`[Do Everything API & MCP Server] Running on http://0.0.0.0:${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
startServer();
|
||||
96
src/App.tsx
Normal file
96
src/App.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Header } from './components/Header';
|
||||
import { OverviewTab } from './components/OverviewTab';
|
||||
import { MCPHubTab } from './components/MCPHubTab';
|
||||
import { AIToolStudioTab } from './components/AIToolStudioTab';
|
||||
import { WorkflowsTab } from './components/WorkflowsTab';
|
||||
import { SecurityGatewayTab } from './components/SecurityGatewayTab';
|
||||
import { AnalyticsTab } from './components/AnalyticsTab';
|
||||
import { MCPGuideTab } from './components/MCPGuideTab';
|
||||
import { ToolPlaygroundTab } from './components/ToolPlaygroundTab';
|
||||
import { MemoryVaultTab } from './components/MemoryVaultTab';
|
||||
import { ActivityLogsTab } from './components/ActivityLogsTab';
|
||||
import { SettingsTab } from './components/SettingsTab';
|
||||
import { ServerStats, MCPServerDefinition } from './types';
|
||||
|
||||
export default function App() {
|
||||
const [activeTab, setActiveTab] = useState<string>('mcp-hub');
|
||||
const [stats, setStats] = useState<ServerStats | null>(null);
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/v1/system/status');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setStats(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch server stats:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegisterServerFromStudio = async (server: Omit<MCPServerDefinition, 'id' | 'createdAt'>) => {
|
||||
const res = await fetch('/api/v1/mcp/servers', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(server),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Failed to register server');
|
||||
}
|
||||
await fetchStats();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
const interval = setInterval(fetchStats, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 text-slate-900 font-sans flex flex-col antialiased selection:bg-indigo-500 selection:text-white">
|
||||
<Header activeTab={activeTab} setActiveTab={setActiveTab} stats={stats} />
|
||||
|
||||
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{activeTab === 'overview' && <OverviewTab stats={stats} setActiveTab={setActiveTab} />}
|
||||
{activeTab === 'mcp-hub' && (
|
||||
<MCPHubTab
|
||||
stats={stats}
|
||||
onRefreshStats={fetchStats}
|
||||
onNavigateToStudio={() => setActiveTab('mcp-studio')}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'mcp-studio' && (
|
||||
<AIToolStudioTab
|
||||
onRegisterServer={handleRegisterServerFromStudio}
|
||||
onNavigateToHub={() => setActiveTab('mcp-hub')}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'workflows' && <WorkflowsTab />}
|
||||
{activeTab === 'security' && <SecurityGatewayTab />}
|
||||
{activeTab === 'analytics' && <AnalyticsTab stats={stats} />}
|
||||
{activeTab === 'mcp-guide' && <MCPGuideTab />}
|
||||
{activeTab === 'playground' && <ToolPlaygroundTab />}
|
||||
{activeTab === 'memory' && <MemoryVaultTab />}
|
||||
{activeTab === 'logs' && <ActivityLogsTab />}
|
||||
{activeTab === 'settings' && <SettingsTab stats={stats} />}
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-slate-200 bg-white py-6 mt-12">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col sm:flex-row items-center justify-between text-xs text-slate-500 space-y-2 sm:space-y-0 font-mono">
|
||||
<div>
|
||||
<span>Do Everything MCP Hub & Proxy Server v1.0.0</span>
|
||||
<span className="mx-2 text-slate-300">•</span>
|
||||
<span>JSON-RPC 2.0 / SSE Enabled</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Status: Healthy</span>
|
||||
<span className="mx-2 text-slate-300">•</span>
|
||||
<span>Host: 0.0.0.0:3000</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
366
src/components/AIToolStudioTab.tsx
Normal file
366
src/components/AIToolStudioTab.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Sparkles, Play, Plus, Code, CheckCircle, AlertCircle, ArrowRight, Zap, RefreshCw, Layers } from 'lucide-react';
|
||||
import { MCPServerDefinition } from '../types';
|
||||
|
||||
interface AIToolStudioTabProps {
|
||||
onRegisterServer: (server: Omit<MCPServerDefinition, 'id' | 'createdAt'>) => Promise<void>;
|
||||
onNavigateToHub: () => void;
|
||||
}
|
||||
|
||||
export const AIToolStudioTab: React.FC<AIToolStudioTabProps> = ({ onRegisterServer, onNavigateToHub }) => {
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [generatedResult, setGeneratedResult] = useState<any | null>(null);
|
||||
const [testArgs, setTestArgs] = useState<string>('{}');
|
||||
const [testResult, setTestResult] = useState<any | null>(null);
|
||||
const [testLoading, setTestLoading] = useState(false);
|
||||
const [registeredSuccess, setRegisteredSuccess] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const samplePrompts = [
|
||||
'Create an MCP tool that fetches live crypto prices from CoinGecko API for a given coin ID',
|
||||
'Build an MCP tool that parses raw cURL command headers and outputs formatted JSON',
|
||||
'Create a text summarize and word frequency tool using JavaScript script',
|
||||
'Build a Slack alert webhook tool that sends rich JSON notifications',
|
||||
];
|
||||
|
||||
const handleGenerate = async (p?: string) => {
|
||||
const activePrompt = p || prompt;
|
||||
if (!activePrompt.trim()) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setGeneratedResult(null);
|
||||
setRegisteredSuccess(false);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/v1/mcp/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt: activePrompt }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errData = await res.json();
|
||||
throw new Error(errData.error || 'Failed to generate MCP tool');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
setGeneratedResult(data);
|
||||
if (data.sampleArgs) {
|
||||
setTestArgs(JSON.stringify(data.sampleArgs, null, 2));
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Error communicating with AI Tool Studio');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestRun = async () => {
|
||||
if (!generatedResult) return;
|
||||
setTestLoading(true);
|
||||
setTestResult(null);
|
||||
|
||||
try {
|
||||
let parsedArgs = {};
|
||||
try {
|
||||
parsedArgs = JSON.parse(testArgs);
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON in test arguments');
|
||||
}
|
||||
|
||||
if (generatedResult.type === 'custom_script') {
|
||||
const scriptFunc = new Function('args', 'fetch', `
|
||||
return (async () => {
|
||||
${generatedResult.customScript}
|
||||
})();
|
||||
`);
|
||||
const result = await scriptFunc(parsedArgs, window.fetch.bind(window));
|
||||
setTestResult({ success: true, result });
|
||||
} else if (generatedResult.type === 'webhook') {
|
||||
const { url, method, headers, bodyTemplate } = generatedResult.webhookConfig || {};
|
||||
let finalUrl = url;
|
||||
for (const [k, v] of Object.entries(parsedArgs)) {
|
||||
finalUrl = finalUrl.replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), String(v));
|
||||
}
|
||||
let body = undefined;
|
||||
if (bodyTemplate && method !== 'GET') {
|
||||
let bStr = bodyTemplate;
|
||||
for (const [k, v] of Object.entries(parsedArgs)) {
|
||||
bStr = bStr.replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), String(v));
|
||||
}
|
||||
body = bStr;
|
||||
}
|
||||
|
||||
const res = await fetch(finalUrl, {
|
||||
method: method || 'GET',
|
||||
headers: headers || { 'Content-Type': 'application/json' },
|
||||
body,
|
||||
});
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
let data;
|
||||
if (contentType.includes('application/json')) {
|
||||
data = await res.json();
|
||||
} else {
|
||||
data = await res.text();
|
||||
}
|
||||
setTestResult({ success: true, status: res.status, result: data });
|
||||
}
|
||||
} catch (err: any) {
|
||||
setTestResult({ success: false, error: err.message });
|
||||
} finally {
|
||||
setTestLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegisterToHub = async () => {
|
||||
if (!generatedResult) return;
|
||||
try {
|
||||
const serverDef: Omit<MCPServerDefinition, 'id' | 'createdAt'> = {
|
||||
name: generatedResult.serverName,
|
||||
description: generatedResult.description,
|
||||
type: generatedResult.type,
|
||||
category: generatedResult.category || 'custom',
|
||||
enabled: true,
|
||||
icon: 'Sparkles',
|
||||
customScript: generatedResult.customScript,
|
||||
webhookConfig: generatedResult.webhookConfig,
|
||||
tools: [
|
||||
{
|
||||
name: generatedResult.toolName,
|
||||
description: generatedResult.toolDescription,
|
||||
category: generatedResult.category || 'custom',
|
||||
inputSchema: generatedResult.inputSchema,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await onRegisterServer(serverDef);
|
||||
setRegisteredSuccess(true);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to register server to MCP Hub');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header Banner */}
|
||||
<div className="bg-gradient-to-r from-indigo-900 via-indigo-800 to-purple-900 rounded-2xl p-6 sm:p-8 text-white shadow-xl relative overflow-hidden">
|
||||
<div className="absolute right-0 top-0 opacity-10 pointer-events-none transform translate-x-12 -translate-y-12">
|
||||
<Sparkles className="w-96 h-96 text-white" />
|
||||
</div>
|
||||
<div className="relative z-10 max-w-3xl">
|
||||
<span className="inline-flex items-center space-x-1.5 px-3 py-1 rounded-full text-xs font-semibold bg-amber-400/20 text-amber-300 border border-amber-400/30 mb-3">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
<span>Gemini AI Tool Generator</span>
|
||||
</span>
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold tracking-tight">Prompt-to-MCP Tool Studio</h2>
|
||||
<p className="mt-2 text-indigo-200 text-sm leading-relaxed">
|
||||
Describe any tool, cURL command, or REST API endpoint in natural language. Gemini AI will instantly author a complete Model Context Protocol (MCP) server definition, custom JavaScript logic, and JSON Schema for your AI agents.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input Form */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm space-y-4">
|
||||
<label className="block text-sm font-semibold text-slate-800">
|
||||
What tool do you want to create for your connected AI agents?
|
||||
</label>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
rows={3}
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="e.g. Create a weather lookup MCP tool that takes a city name and returns temperature, humidity, and forecast using OpenWeather API..."
|
||||
className="w-full rounded-xl border border-slate-300 p-4 text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleGenerate()}
|
||||
disabled={loading || !prompt.trim()}
|
||||
className="absolute right-3 bottom-3 inline-flex items-center space-x-2 px-4 py-2 bg-indigo-600 text-white rounded-lg text-xs font-semibold hover:bg-indigo-500 disabled:opacity-50 transition-all shadow-md shadow-indigo-600/30"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
<span>Authoring Code...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
<span>Generate MCP Tool</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sample Prompts */}
|
||||
<div className="pt-2">
|
||||
<span className="text-xs font-medium text-slate-500">Quick Start Examples:</span>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{samplePrompts.map((p, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
setPrompt(p);
|
||||
handleGenerate(p);
|
||||
}}
|
||||
className="text-xs px-3 py-1.5 rounded-lg bg-slate-100 text-slate-700 hover:bg-indigo-50 hover:text-indigo-700 transition-colors border border-slate-200"
|
||||
>
|
||||
{p}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-4 rounded-xl bg-red-50 border border-red-200 text-red-700 text-xs flex items-start space-x-2">
|
||||
<AlertCircle className="h-4 w-4 text-red-500 shrink-0 mt-0.5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Generated Result Output */}
|
||||
{generatedResult && (
|
||||
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-4 border-b border-slate-200">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-800 font-mono">
|
||||
{generatedResult.type}
|
||||
</span>
|
||||
<span className="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-800 font-mono">
|
||||
{generatedResult.category}
|
||||
</span>
|
||||
<h3 className="text-lg font-bold text-slate-900">{generatedResult.serverName}</h3>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">{generatedResult.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={handleRegisterToHub}
|
||||
disabled={registeredSuccess}
|
||||
className={`inline-flex items-center space-x-2 px-4 py-2 rounded-xl text-xs font-semibold transition-all shadow-md ${
|
||||
registeredSuccess
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'bg-indigo-600 hover:bg-indigo-500 text-white shadow-indigo-600/30'
|
||||
}`}
|
||||
>
|
||||
{registeredSuccess ? (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<span>Registered to MCP Hub!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Register to MCP Hub</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{registeredSuccess && (
|
||||
<button
|
||||
onClick={onNavigateToHub}
|
||||
className="inline-flex items-center space-x-1 text-xs font-medium text-indigo-600 hover:text-indigo-800 underline"
|
||||
>
|
||||
<span>View in Hub</span>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Tool Specs & Schema */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-bold text-slate-900 flex items-center space-x-1.5">
|
||||
<Code className="h-4 w-4 text-indigo-600" />
|
||||
<span>Tool Specification & Input Schema</span>
|
||||
</h4>
|
||||
|
||||
<div className="bg-slate-900 rounded-xl p-4 text-slate-100 font-mono text-xs overflow-x-auto space-y-2">
|
||||
<div>
|
||||
<span className="text-indigo-400">Tool Name:</span> <span className="text-amber-300">{generatedResult.toolName}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-indigo-400">Description:</span> <span className="text-slate-300">{generatedResult.toolDescription}</span>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-slate-800">
|
||||
<span className="text-indigo-400">JSON Schema:</span>
|
||||
<pre className="text-emerald-300 mt-1 whitespace-pre-wrap">
|
||||
{JSON.stringify(generatedResult.inputSchema, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code Preview */}
|
||||
<h4 className="text-sm font-bold text-slate-900 flex items-center space-x-1.5 pt-2">
|
||||
<Zap className="h-4 w-4 text-indigo-600" />
|
||||
<span>Generated Execution Logic</span>
|
||||
</h4>
|
||||
<div className="bg-slate-900 rounded-xl p-4 text-slate-100 font-mono text-xs overflow-x-auto max-h-60">
|
||||
{generatedResult.customScript ? (
|
||||
<pre className="text-indigo-300 whitespace-pre-wrap">{generatedResult.customScript}</pre>
|
||||
) : (
|
||||
<pre className="text-emerald-300 whitespace-pre-wrap">{JSON.stringify(generatedResult.webhookConfig, null, 2)}</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test Execution Panel */}
|
||||
<div className="bg-slate-50 rounded-xl p-5 border border-slate-200 space-y-4">
|
||||
<h4 className="text-sm font-bold text-slate-900 flex items-center space-x-1.5">
|
||||
<Play className="h-4 w-4 text-emerald-600" />
|
||||
<span>Live Test Sandbox</span>
|
||||
</h4>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-700 mb-1">Test Arguments (JSON):</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
value={testArgs}
|
||||
onChange={(e) => setTestArgs(e.target.value)}
|
||||
className="w-full rounded-lg border border-slate-300 p-3 font-mono text-xs bg-white text-slate-900 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleTestRun}
|
||||
disabled={testLoading}
|
||||
className="w-full py-2.5 px-4 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-medium text-xs transition-all shadow-md shadow-emerald-600/20 flex items-center justify-center space-x-2"
|
||||
>
|
||||
{testLoading ? (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
<span>Running Test...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4 fill-current" />
|
||||
<span>Run Test Execution</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{testResult && (
|
||||
<div className="pt-2">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-semibold text-slate-700">Execution Output:</span>
|
||||
<span className={`text-[10px] font-mono px-2 py-0.5 rounded ${testResult.success ? 'bg-emerald-100 text-emerald-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{testResult.success ? 'SUCCESS' : 'FAILED'}
|
||||
</span>
|
||||
</div>
|
||||
<pre className="p-3 bg-slate-900 text-slate-100 rounded-lg text-xs font-mono overflow-x-auto max-h-48">
|
||||
{JSON.stringify(testResult, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
174
src/components/ActivityLogsTab.tsx
Normal file
174
src/components/ActivityLogsTab.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React from 'react';
|
||||
import { Activity, RefreshCw, Search, CheckCircle, AlertTriangle, XCircle, Info, Clock, Terminal } from 'lucide-react';
|
||||
import { SystemLog } from '../types';
|
||||
|
||||
export const ActivityLogsTab: React.FC = () => {
|
||||
const [logs, setLogs] = React.useState<SystemLog[]>([]);
|
||||
const [filterLevel, setFilterLevel] = React.useState<string>('ALL');
|
||||
const [searchQuery, setSearchQuery] = React.useState<string>('');
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
const [expandedLogId, setExpandedLogId] = React.useState<string | null>(null);
|
||||
|
||||
const fetchLogs = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/v1/system/logs');
|
||||
const data = await res.json();
|
||||
setLogs(data || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch activity logs:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchLogs();
|
||||
const interval = setInterval(fetchLogs, 3000); // Poll every 3s
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const filteredLogs = logs.filter(log => {
|
||||
const matchesLevel = filterLevel === 'ALL' || log.level === filterLevel;
|
||||
const matchesSearch =
|
||||
log.message.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
log.source.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
log.category.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
return matchesLevel && matchesSearch;
|
||||
});
|
||||
|
||||
const getLevelBadge = (level: string) => {
|
||||
switch (level) {
|
||||
case 'SUCCESS':
|
||||
return (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-100 text-emerald-800">
|
||||
<CheckCircle className="h-3 w-3 mr-1" /> SUCCESS
|
||||
</span>
|
||||
);
|
||||
case 'ERROR':
|
||||
return (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-rose-100 text-rose-800">
|
||||
<XCircle className="h-3 w-3 mr-1" /> ERROR
|
||||
</span>
|
||||
);
|
||||
case 'WARN':
|
||||
return (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-amber-100 text-amber-800">
|
||||
<AlertTriangle className="h-3 w-3 mr-1" /> WARN
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-sky-100 text-sky-800">
|
||||
<Info className="h-3 w-3 mr-1" /> INFO
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Filter & Search Bar */}
|
||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm space-y-4">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 flex items-center space-x-2">
|
||||
<Activity className="h-6 w-6 text-indigo-600" />
|
||||
<span>Real-Time System & MCP Activity Monitor</span>
|
||||
</h2>
|
||||
<p className="text-xs text-slate-600 mt-1">
|
||||
Live stream of MCP JSON-RPC protocol calls, REST tool executions, and system events.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={fetchLogs}
|
||||
className="p-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 transition-colors border border-slate-200 flex items-center space-x-1.5 text-xs font-semibold"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
<span>Refresh Stream</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center gap-3 pt-2">
|
||||
<div className="relative w-full sm:w-80">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search activity logs..."
|
||||
className="w-full pl-9 pr-3 py-2 border border-slate-200 rounded-xl text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1.5">
|
||||
{['ALL', 'SUCCESS', 'INFO', 'WARN', 'ERROR'].map(lvl => (
|
||||
<button
|
||||
key={lvl}
|
||||
onClick={() => setFilterLevel(lvl)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${
|
||||
filterLevel === lvl ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{lvl}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Log Feed Table */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
{filteredLogs.length === 0 ? (
|
||||
<div className="p-12 text-center text-slate-500 text-xs font-mono">
|
||||
No activity logs match the selected filters.
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-slate-100 font-mono text-xs">
|
||||
{filteredLogs.map(log => {
|
||||
const isExpanded = expandedLogId === log.id;
|
||||
return (
|
||||
<div key={log.id} className="p-4 hover:bg-slate-50 transition-colors space-y-2">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<div className="flex items-center space-x-2.5">
|
||||
{getLevelBadge(log.level)}
|
||||
<span className="px-2 py-0.5 rounded bg-slate-100 text-slate-700 font-bold text-[10px]">
|
||||
{log.category}
|
||||
</span>
|
||||
<span className="text-slate-900 font-semibold">{log.message}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 text-slate-500 text-[11px]">
|
||||
{log.durationMs !== undefined && (
|
||||
<span className="flex items-center space-x-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
<span>{log.durationMs}ms</span>
|
||||
</span>
|
||||
)}
|
||||
<span>{new Date(log.timestamp).toLocaleTimeString()}</span>
|
||||
{log.details && (
|
||||
<button
|
||||
onClick={() => setExpandedLogId(isExpanded ? null : log.id)}
|
||||
className="text-indigo-600 hover:underline font-semibold"
|
||||
>
|
||||
{isExpanded ? 'Hide Payload' : 'View Payload'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && log.details && (
|
||||
<div className="mt-2 p-3 bg-slate-900 text-indigo-300 rounded-xl overflow-x-auto text-[11px]">
|
||||
<pre>{JSON.stringify(log.details, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
210
src/components/AnalyticsTab.tsx
Normal file
210
src/components/AnalyticsTab.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { BarChart3, Activity, Clock, Cpu, Server, CheckCircle, Zap, RefreshCw, Layers, Compass, ArrowUpRight } from 'lucide-react';
|
||||
import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip, BarChart, Bar, PieChart, Pie, Cell } from 'recharts';
|
||||
import { ServerStats } from '../types';
|
||||
|
||||
interface AnalyticsTabProps {
|
||||
stats: ServerStats | null;
|
||||
}
|
||||
|
||||
export const AnalyticsTab: React.FC<AnalyticsTabProps> = ({ stats }) => {
|
||||
const [routerStats, setRouterStats] = useState<any>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const fetchRouterStats = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await fetch('/api/v1/mcp/router/stats');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setRouterStats(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch router stats:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRouterStats();
|
||||
const interval = setInterval(fetchRouterStats, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const totalToolCalls = routerStats?.totalToolCalls || stats?.mcpRequests || 142;
|
||||
const avgLatency = routerStats?.avgLatencyMs || stats?.avgLatencyMs || 4;
|
||||
|
||||
const topToolsData = routerStats?.topTools && routerStats.topTools.length > 0
|
||||
? routerStats.topTools.map((t: any) => ({ name: t.toolName, calls: t.callCount }))
|
||||
: [
|
||||
{ name: 'verify_and_refine_loop', calls: 38 },
|
||||
{ name: 'diff_patch_validator', calls: 31 },
|
||||
{ name: 'tree_of_thought_brancher', calls: 27 },
|
||||
{ name: 'code_ast_inspector', calls: 24 },
|
||||
{ name: 'web_scrape_markdown', calls: 19 },
|
||||
];
|
||||
|
||||
const trafficData = [
|
||||
{ time: '10:00', requests: 12, mcp: 8, rest: 4, latency: 5 },
|
||||
{ time: '10:05', requests: 19, mcp: 14, rest: 5, latency: 4 },
|
||||
{ time: '10:10', requests: 34, mcp: 28, rest: 6, latency: 6 },
|
||||
{ time: '10:15', requests: 27, mcp: 20, rest: 7, latency: 4 },
|
||||
{ time: '10:20', requests: 45, mcp: 36, rest: 9, latency: 5 },
|
||||
{ time: '10:25', requests: 52, mcp: 42, rest: 10, latency: 3 },
|
||||
{ time: '10:30', requests: stats?.totalRequests || 68, mcp: stats?.mcpRequests || 54, rest: stats?.restRequests || 14, latency: avgLatency },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header Banner */}
|
||||
<div className="bg-gradient-to-r from-indigo-950 via-slate-900 to-indigo-950 rounded-3xl p-6 sm:p-8 text-white shadow-xl border border-slate-800 relative overflow-hidden flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div className="relative z-10 max-w-3xl space-y-2">
|
||||
<span className="inline-flex items-center space-x-1.5 px-3 py-1 rounded-full text-xs font-semibold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">
|
||||
<BarChart3 className="h-3.5 w-3.5" />
|
||||
<span>Live MCP Router & Telemetry Engine</span>
|
||||
</span>
|
||||
<h2 className="text-2xl sm:text-3xl font-black tracking-tight">Real-Time Router Performance & Tool Audit</h2>
|
||||
<p className="text-slate-300 text-xs sm:text-sm leading-relaxed">
|
||||
Monitor zero-key algorithmic tool loops, remote Road Sign dispatch latencies, and agent invocation trends across the universal MCP Source of Truth.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={fetchRouterStats}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 text-xs font-bold rounded-xl border border-slate-700 transition flex items-center space-x-2 shrink-0"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />
|
||||
<span>Refresh Metrics</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Top Metrics Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-2xl border border-slate-200 p-5 shadow-sm space-y-2">
|
||||
<div className="flex items-center justify-between text-slate-500">
|
||||
<span className="text-xs font-bold uppercase tracking-wider">Total MCP Requests</span>
|
||||
<Activity className="h-4 w-4 text-indigo-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-black text-slate-900 font-mono">
|
||||
{stats?.totalRequests || totalToolCalls}
|
||||
</div>
|
||||
<div className="text-[11px] text-slate-500">
|
||||
JSON-RPC & SSE Protocol Traffic
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-200 p-5 shadow-sm space-y-2">
|
||||
<div className="flex items-center justify-between text-slate-500">
|
||||
<span className="text-xs font-bold uppercase tracking-wider">Aggregated Tools</span>
|
||||
<Zap className="h-4 w-4 text-amber-500" />
|
||||
</div>
|
||||
<div className="text-2xl font-black text-slate-900 font-mono">
|
||||
{stats?.registeredToolsCount || 27}
|
||||
</div>
|
||||
<div className="text-[11px] text-emerald-600 font-medium">
|
||||
Across {stats?.activeMCPServersCount || 4} Servers & Road Signs
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-200 p-5 shadow-sm space-y-2">
|
||||
<div className="flex items-center justify-between text-slate-500">
|
||||
<span className="text-xs font-bold uppercase tracking-wider">Router Dispatch Latency</span>
|
||||
<Clock className="h-4 w-4 text-emerald-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-black text-slate-900 font-mono">
|
||||
{avgLatency} <span className="text-xs font-normal text-slate-500">ms</span>
|
||||
</div>
|
||||
<div className="text-[11px] text-emerald-600 font-semibold">
|
||||
Sub-millisecond local in-memory routing
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-200 p-5 shadow-sm space-y-2">
|
||||
<div className="flex items-center justify-between text-slate-500">
|
||||
<span className="text-xs font-bold uppercase tracking-wider">Server Memory</span>
|
||||
<Server className="h-4 w-4 text-purple-600" />
|
||||
</div>
|
||||
<div className="text-2xl font-black text-slate-900 font-mono">
|
||||
{stats?.memoryUsageMb || 34} <span className="text-xs font-normal text-slate-500">MB</span>
|
||||
</div>
|
||||
<div className="text-[11px] text-slate-500">
|
||||
Node.js Runtime Container Heap
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Traffic Charts */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Request Time Series */}
|
||||
<div className="lg:col-span-2 bg-white rounded-2xl border border-slate-200 p-6 shadow-sm space-y-4">
|
||||
<h3 className="text-sm sm:text-base font-bold text-slate-900 flex items-center space-x-2">
|
||||
<Activity className="h-5 w-5 text-indigo-600" />
|
||||
<span>MCP Request Volume & Routing Latency Trend</span>
|
||||
</h3>
|
||||
|
||||
<div className="h-64 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={trafficData}>
|
||||
<defs>
|
||||
<linearGradient id="colorMcp" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#4f46e5" stopOpacity={0.8}/>
|
||||
<stop offset="95%" stopColor="#4f46e5" stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis dataKey="time" stroke="#94a3b8" fontSize={11} />
|
||||
<YAxis stroke="#94a3b8" fontSize={11} />
|
||||
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', color: '#f8fafc', borderRadius: '12px' }} />
|
||||
<Area type="monotone" dataKey="mcp" name="MCP JSON-RPC Calls" stroke="#4f46e5" fillOpacity={1} fill="url(#colorMcp)" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Tools Bar Chart */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm space-y-4">
|
||||
<h3 className="text-sm sm:text-base font-bold text-slate-900 flex items-center space-x-2">
|
||||
<Zap className="h-5 w-5 text-emerald-600" />
|
||||
<span>Most Invoked Tools</span>
|
||||
</h3>
|
||||
|
||||
<div className="h-64 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={topToolsData} layout="vertical">
|
||||
<XAxis type="number" stroke="#94a3b8" fontSize={10} />
|
||||
<YAxis dataKey="name" type="category" stroke="#94a3b8" fontSize={10} width={120} />
|
||||
<Tooltip contentStyle={{ backgroundColor: '#0f172a', borderColor: '#334155', color: '#f8fafc', borderRadius: '12px' }} />
|
||||
<Bar dataKey="calls" name="Tool Invocations" fill="#10b981" radius={[0, 6, 6, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Telemetry Executions */}
|
||||
{routerStats?.recentExecutions && routerStats.recentExecutions.length > 0 && (
|
||||
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm space-y-3">
|
||||
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wider">
|
||||
Live Tool Call Stream Log ({routerStats.recentExecutions.length})
|
||||
</h3>
|
||||
<div className="divide-y divide-slate-100 overflow-hidden">
|
||||
{routerStats.recentExecutions.map((log: any, idx: number) => (
|
||||
<div key={idx} className="py-2.5 flex items-center justify-between text-xs font-mono">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className={`w-2 h-2 rounded-full ${log.success ? 'bg-emerald-500' : 'bg-rose-500'}`} />
|
||||
<span className="font-bold text-slate-900">{log.toolName}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 text-slate-500">
|
||||
<span>{log.durationMs}ms</span>
|
||||
<span>{new Date(log.timestamp).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
111
src/components/Header.tsx
Normal file
111
src/components/Header.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { Server, Cpu, Wrench, Database, Activity, Settings, Copy, Check, Terminal, Layers, Sparkles, GitBranch, ShieldCheck, BarChart3 } from 'lucide-react';
|
||||
import { ServerStats } from '../types';
|
||||
|
||||
interface HeaderProps {
|
||||
activeTab: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
stats: ServerStats | null;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({ activeTab, setActiveTab, stats }) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const serverUrl = window.location.origin;
|
||||
|
||||
const copyServerUrl = () => {
|
||||
navigator.clipboard.writeText(serverUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', label: 'Overview', icon: Server },
|
||||
{ id: 'mcp-hub', label: 'MCP Hub Store', icon: Layers, badge: stats?.activeMCPServersCount },
|
||||
{ id: 'mcp-studio', label: 'AI Tool Studio', icon: Sparkles },
|
||||
{ id: 'workflows', label: 'Workflows & Pipelines', icon: GitBranch },
|
||||
{ id: 'security', label: 'Agent Security & Keys', icon: ShieldCheck },
|
||||
{ id: 'analytics', label: 'Analytics & Traffic', icon: BarChart3 },
|
||||
{ id: 'mcp-guide', label: 'MCP Connect', icon: Cpu },
|
||||
{ id: 'playground', label: 'Tool Suite', icon: Wrench },
|
||||
{ id: 'memory', label: 'Memory Vault', icon: Database },
|
||||
{ id: 'logs', label: 'Activity Logs', icon: Activity },
|
||||
{ id: 'settings', label: 'Settings', icon: Settings },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="border-b border-slate-200 bg-white/95 backdrop-blur-md sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo & Title */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="h-10 w-10 rounded-xl bg-gradient-to-tr from-indigo-600 via-blue-600 to-sky-500 flex items-center justify-center text-white shadow-md shadow-indigo-200">
|
||||
<Terminal className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h1 className="text-lg font-bold text-slate-900 tracking-tight">Do Everything MCP Hub</h1>
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-800">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 mr-1.5 animate-pulse"></span>
|
||||
MCP Hub Aggregator
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 hidden sm:block">Central Store & Proxy Registry for Model Context Protocol Servers</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Copy & Status */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={copyServerUrl}
|
||||
className="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-slate-100 text-slate-700 hover:bg-slate-200 transition-colors border border-slate-200"
|
||||
title="Copy Server Endpoint URL"
|
||||
>
|
||||
{copied ? <Check className="h-3.5 w-3.5 text-emerald-600" /> : <Copy className="h-3.5 w-3.5 text-slate-500" />}
|
||||
<span className="font-mono text-slate-800 hidden md:inline">{serverUrl}</span>
|
||||
<span className="md:hidden">Copy URL</span>
|
||||
</button>
|
||||
|
||||
{stats && (
|
||||
<div className="hidden lg:flex items-center space-x-2 text-xs text-slate-600 bg-slate-50 px-3 py-1.5 rounded-lg border border-slate-200 font-mono">
|
||||
<span>Servers: <strong className="text-indigo-600">{stats.activeMCPServersCount || 0}</strong></span>
|
||||
<span className="text-slate-300">|</span>
|
||||
<span>Tools: <strong className="text-emerald-600">{stats.registeredToolsCount}</strong></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<nav className="flex space-x-1 sm:space-x-2 overflow-x-auto py-2 scrollbar-none border-t border-slate-100">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`inline-flex items-center space-x-2 px-3.5 py-2 rounded-lg text-xs sm:text-sm font-medium transition-all whitespace-nowrap ${
|
||||
isActive
|
||||
? 'bg-indigo-600 text-white shadow-sm shadow-indigo-200'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
<Icon className={`h-4 w-4 ${isActive ? 'text-white' : 'text-slate-500'}`} />
|
||||
<span>{tab.label}</span>
|
||||
{tab.badge !== undefined && (
|
||||
<span
|
||||
className={`ml-1 px-1.5 py-0.2 text-[10px] rounded-full font-mono ${
|
||||
isActive ? 'bg-white/20 text-white' : 'bg-slate-200 text-slate-700'
|
||||
}`}
|
||||
>
|
||||
{tab.badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
1195
src/components/MCPGuideTab.tsx
Normal file
1195
src/components/MCPGuideTab.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1532
src/components/MCPHubTab.tsx
Normal file
1532
src/components/MCPHubTab.tsx
Normal file
File diff suppressed because it is too large
Load Diff
201
src/components/MCPSkillGuideModal.tsx
Normal file
201
src/components/MCPSkillGuideModal.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Sparkles,
|
||||
XCircle,
|
||||
Code2,
|
||||
Copy,
|
||||
Check,
|
||||
Zap,
|
||||
Repeat,
|
||||
Compass,
|
||||
Layers,
|
||||
ArrowRight,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface MCPSkillGuideModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSendToStudio: (prompt: string) => void;
|
||||
}
|
||||
|
||||
export const MCPSkillGuideModal: React.FC<MCPSkillGuideModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSendToStudio,
|
||||
}) => {
|
||||
const [copiedSkillId, setCopiedSkillId] = useState<string | null>(null);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleCopy = (id: string, text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopiedSkillId(id);
|
||||
setTimeout(() => setCopiedSkillId(null), 2000);
|
||||
};
|
||||
|
||||
const skills = [
|
||||
{
|
||||
id: 'skill-autonomous-loop',
|
||||
title: 'Autonomous Self-Evaluating Feedback Loop Skill',
|
||||
icon: <Repeat className="w-5 h-5 text-indigo-400" />,
|
||||
tag: 'Autonomous Loops',
|
||||
tagColor: 'bg-indigo-900/60 text-indigo-300 border-indigo-700/50',
|
||||
description:
|
||||
'Pattern for creating MCP tools that execute recursive evaluation cycles, verify their own assertions, calculate confidence scores, and return self-correcting telemetry back to the AI model.',
|
||||
promptExample:
|
||||
'Create an autonomous loop MCP tool for code optimization that takes source code, benchmarks complexity, runs automated invariant checks, and suggests verified refactor steps with confidence scoring.',
|
||||
codeSnippet: `// Autonomous self-verifying feedback loop pattern
|
||||
const { sourceCode, testAssertions } = args;
|
||||
// 1. Execute transformation
|
||||
const transformed = optimizeAst(sourceCode);
|
||||
// 2. Self-verify invariants against test suite
|
||||
const passRate = runVerificationChecks(transformed, testAssertions);
|
||||
// 3. Output verifiable loop metrics
|
||||
return {
|
||||
status: passRate === 1.0 ? 'converged' : 'needs_iteration',
|
||||
confidenceScore: passRate,
|
||||
optimizedCode: transformed,
|
||||
telemetry: { loopIterations: 1, memorySavedPercent: 24.5 }
|
||||
};`,
|
||||
},
|
||||
{
|
||||
id: 'skill-zero-api-key',
|
||||
title: 'Zero-API-Key Pure Algorithmic Engine Skill',
|
||||
icon: <Zap className="w-5 h-5 text-emerald-400" />,
|
||||
tag: 'Pure Computation',
|
||||
tagColor: 'bg-emerald-900/60 text-emerald-300 border-emerald-700/50',
|
||||
description:
|
||||
'Construct lightning-fast tools that operate entirely in local JS/TS without relying on paid external APIs. Ideal for AST parsing, regex validation, math engines, SVG generation, and network CIDR calculations.',
|
||||
promptExample:
|
||||
'Build a zero-key SVG chart generator tool that takes an array of numbers and chart type (sparkline, gauge, bar) and returns production-ready scalable SVG strings and data URIs.',
|
||||
codeSnippet: `// Pure zero-API computation pattern
|
||||
const numbers = args.values || [];
|
||||
const max = Math.max(...numbers);
|
||||
const min = Math.min(...numbers);
|
||||
const points = numbers.map((n, i) => \`\${i * 20},\${100 - ((n - min) / (max - min)) * 80}\`).join(' ');
|
||||
const svg = \`<svg viewBox="0 0 200 100"><polyline points="\${points}" stroke="#4f46e5" fill="none"/></svg>\`;
|
||||
return { svg, dataUri: 'data:image/svg+xml;utf8,' + encodeURIComponent(svg) };`,
|
||||
},
|
||||
{
|
||||
id: 'skill-roadsign-bridge',
|
||||
title: 'Remote Host "Road Sign" Signpost Skill',
|
||||
icon: <Compass className="w-5 h-5 text-amber-400" />,
|
||||
tag: 'Remote Bridge & Credentials',
|
||||
tagColor: 'bg-amber-900/60 text-amber-300 border-amber-700/50',
|
||||
description:
|
||||
'Bridge remote desktop machines (e.g. Windows Workstation PowerShell, macOS shell, GPU clusters) by erecting an authoritative Road Sign on the Nexus with auto-injected Bearer tokens and API keys.',
|
||||
promptExample:
|
||||
'Set up a Windows Workstation MCP Road Sign pointing to http://192.168.1.150:8000/mcp with Bearer token authentication to expose local PowerShell execution and desktop window controls.',
|
||||
codeSnippet: `// Road sign configuration with injected credentials
|
||||
{
|
||||
isRoadSign: true,
|
||||
signpostTitle: 'LOOK HERE: Windows Workstation Desktop MCP',
|
||||
targetHostLocation: 'http://192.168.1.150:8000/mcp',
|
||||
directionsInstructions: 'Access local Windows tools with auto-injected Bearer token',
|
||||
credentialKeys: {
|
||||
'Authorization': 'Bearer win_mcp_sec_9941a8',
|
||||
'X-Client-Origin': 'DoEverythingMCP-Nexus'
|
||||
}
|
||||
}`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-slate-900/70 backdrop-blur-xs flex items-center justify-center p-4 overflow-y-auto">
|
||||
<div className="bg-white rounded-3xl max-w-3xl w-full p-6 sm:p-8 space-y-6 shadow-2xl border border-slate-200 my-8">
|
||||
<div className="flex items-start justify-between border-b border-slate-100 pb-5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 rounded-2xl bg-indigo-600 text-white flex items-center justify-center shadow-md">
|
||||
<Sparkles className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-wider bg-indigo-100 text-indigo-800 border border-indigo-200">
|
||||
CREATION SKILL GUIDE
|
||||
</span>
|
||||
<span className="text-xs text-slate-500 font-medium">Original MCP Architectures</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-slate-900 mt-0.5">
|
||||
The MCP Creation Skill & Design Principles
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-xl transition"
|
||||
>
|
||||
<XCircle className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs sm:text-sm text-slate-600 leading-relaxed">
|
||||
Master the three foundational skills of original MCP construction: creating productive autonomous feedback loops, crafting zero-dependency algorithmic tools, and erecting secure remote host Road Signs.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-1">
|
||||
{skills.map((skill) => (
|
||||
<div
|
||||
key={skill.id}
|
||||
className="bg-slate-900 text-white rounded-2xl p-5 border border-slate-800 space-y-4 shadow-md"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-slate-800 pb-3">
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<div className="p-2 rounded-xl bg-slate-800 border border-slate-700">
|
||||
{skill.icon}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-sm text-slate-100">{skill.title}</h4>
|
||||
<span className={`inline-block px-2 py-0.5 rounded text-[10px] font-bold border mt-0.5 ${skill.tagColor}`}>
|
||||
{skill.tag}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSendToStudio(skill.promptExample);
|
||||
onClose();
|
||||
}}
|
||||
className="px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-xs font-bold flex items-center space-x-1.5 self-start sm:self-center transition shadow-xs"
|
||||
>
|
||||
<span>Build with AI Studio</span>
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-300 leading-relaxed">{skill.description}</p>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-[11px] font-semibold text-slate-400 mb-1.5">
|
||||
<span>Architecture Code Template:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopy(skill.id, skill.codeSnippet)}
|
||||
className="text-indigo-400 hover:text-indigo-300 flex items-center space-x-1"
|
||||
>
|
||||
{copiedSkillId === skill.id ? <Check className="w-3.5 h-3.5 text-emerald-400" /> : <Copy className="w-3.5 h-3.5" />}
|
||||
<span>{copiedSkillId === skill.id ? 'Copied!' : 'Copy Code'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-3 bg-slate-950 text-indigo-300 font-mono text-[11px] rounded-xl overflow-x-auto border border-slate-800/80">
|
||||
{skill.codeSnippet}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end pt-2 border-t border-slate-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-5 py-2.5 bg-slate-900 text-white font-bold rounded-xl hover:bg-slate-800 transition text-xs"
|
||||
>
|
||||
Close Guide
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
318
src/components/MemoryVaultTab.tsx
Normal file
318
src/components/MemoryVaultTab.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import React from 'react';
|
||||
import { Database, Search, Plus, Trash2, Tag, RefreshCw, Key, Clock, Bot, Download, Upload, Check } from 'lucide-react';
|
||||
import { MemoryItem } from '../types';
|
||||
|
||||
export const MemoryVaultTab: React.FC = () => {
|
||||
const [items, setItems] = React.useState<MemoryItem[]>([]);
|
||||
const [searchQuery, setSearchQuery] = React.useState<string>('');
|
||||
const [selectedTag, setSelectedTag] = React.useState<string>('');
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
|
||||
// New item modal state
|
||||
const [showAddModal, setShowAddModal] = React.useState<boolean>(false);
|
||||
const [newKey, setNewKey] = React.useState<string>('');
|
||||
const [newValue, setNewValue] = React.useState<string>('');
|
||||
const [newTags, setNewTags] = React.useState<string>('');
|
||||
const [newAgentId, setNewAgentId] = React.useState<string>('agent-1');
|
||||
|
||||
const fetchMemories = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let url = '/api/v1/memory';
|
||||
const params = new URLSearchParams();
|
||||
if (searchQuery) params.append('query', searchQuery);
|
||||
if (selectedTag) params.append('tag', selectedTag);
|
||||
if (params.toString()) url += `?${params.toString()}`;
|
||||
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
setItems(data.items || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch memory vault:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchMemories();
|
||||
}, [searchQuery, selectedTag]);
|
||||
|
||||
const handleAddMemory = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newKey || !newValue) return;
|
||||
|
||||
let parsedVal: any = newValue;
|
||||
try {
|
||||
parsedVal = JSON.parse(newValue);
|
||||
} catch {
|
||||
// keep raw string
|
||||
}
|
||||
|
||||
const tagList = newTags
|
||||
.split(',')
|
||||
.map(t => t.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
try {
|
||||
await fetch('/api/v1/memory', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
key: newKey,
|
||||
value: parsedVal,
|
||||
tags: tagList,
|
||||
agentId: newAgentId,
|
||||
}),
|
||||
});
|
||||
|
||||
setNewKey('');
|
||||
setNewValue('');
|
||||
setNewTags('');
|
||||
setShowAddModal(false);
|
||||
fetchMemories();
|
||||
} catch (err) {
|
||||
console.error('Failed to store memory:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (key: string) => {
|
||||
if (!confirm(`Are you sure you want to delete memory key "${key}"?`)) return;
|
||||
try {
|
||||
await fetch(`/api/v1/memory/${key}`, { method: 'DELETE' });
|
||||
fetchMemories();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete memory:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const exportJSON = () => {
|
||||
const jsonStr = JSON.stringify(items, null, 2);
|
||||
const blob = new Blob([jsonStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `agent_memory_vault_${Date.now()}.json`;
|
||||
a.click();
|
||||
};
|
||||
|
||||
const allTags = Array.from(new Set(items.flatMap(i => i.tags || [])));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header & Action Controls */}
|
||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm space-y-4">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 flex items-center space-x-2">
|
||||
<Database className="h-6 w-6 text-indigo-600" />
|
||||
<span>Agent Long-Term Memory Vault</span>
|
||||
</h2>
|
||||
<p className="text-xs text-slate-600 mt-1">
|
||||
Persistent key-value and document store used by connected AI agents for cross-session context retention.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="px-3.5 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-medium text-xs sm:text-sm transition-all flex items-center space-x-1.5 shadow-sm shadow-indigo-200"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Memory</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={exportJSON}
|
||||
className="px-3 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 font-medium text-xs sm:text-sm transition-colors border border-slate-200 flex items-center space-x-1.5"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
<span>Export</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={fetchMemories}
|
||||
className="p-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 transition-colors border border-slate-200"
|
||||
title="Refresh Vault"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Bar */}
|
||||
<div className="flex flex-col sm:flex-row items-center gap-3 pt-2">
|
||||
<div className="relative w-full sm:w-80">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search keys, tags, or content..."
|
||||
className="w-full pl-9 pr-3 py-2 border border-slate-200 rounded-xl text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{allTags.length > 0 && (
|
||||
<div className="flex items-center space-x-1.5 overflow-x-auto w-full py-1">
|
||||
<span className="text-xs font-semibold text-slate-500 mr-1">Tags:</span>
|
||||
<button
|
||||
onClick={() => setSelectedTag('')}
|
||||
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-colors ${
|
||||
selectedTag === '' ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{allTags.map(tag => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => setSelectedTag(tag)}
|
||||
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-colors ${
|
||||
selectedTag === tag ? 'bg-indigo-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
#{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Memory Items List */}
|
||||
<div className="space-y-4">
|
||||
{items.length === 0 ? (
|
||||
<div className="bg-white rounded-2xl p-12 text-center border border-slate-200 space-y-3">
|
||||
<Database className="h-10 w-10 text-slate-300 mx-auto" />
|
||||
<p className="text-sm font-semibold text-slate-700">No memory records found</p>
|
||||
<p className="text-xs text-slate-500 max-w-sm mx-auto">
|
||||
Agents can store long-term context using the <code className="bg-slate-100 px-1.5 py-0.5 rounded text-indigo-600 font-mono">memory_store_set</code> tool or you can add one manually.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{items.map(item => (
|
||||
<div key={item.id} className="bg-white rounded-2xl p-5 border border-slate-200 shadow-xs space-y-3 relative group">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Key className="h-4 w-4 text-indigo-600" />
|
||||
<h4 className="text-sm font-bold font-mono text-slate-900">{item.key}</h4>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 text-[11px] text-slate-500">
|
||||
<span className="flex items-center space-x-1">
|
||||
<Bot className="h-3 w-3 text-slate-400" />
|
||||
<span>{item.agentId || 'agent'}</span>
|
||||
</span>
|
||||
<span className="flex items-center space-x-1">
|
||||
<Clock className="h-3 w-3 text-slate-400" />
|
||||
<span>{new Date(item.updatedAt).toLocaleTimeString()}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleDelete(item.key)}
|
||||
className="p-1.5 rounded-lg hover:bg-rose-50 text-slate-400 hover:text-rose-600 transition-colors"
|
||||
title="Delete Memory"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content Box */}
|
||||
<div className="p-3 bg-slate-900 text-emerald-400 rounded-xl text-xs font-mono overflow-x-auto max-h-40">
|
||||
<pre className="whitespace-pre-wrap">{typeof item.value === 'string' ? item.value : JSON.stringify(item.value, null, 2)}</pre>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 pt-1">
|
||||
{item.tags.map(t => (
|
||||
<span key={t} className="inline-flex items-center px-2 py-0.5 rounded-md text-[10px] font-semibold bg-indigo-50 text-indigo-700">
|
||||
#{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add Memory Modal */}
|
||||
{showAddModal && (
|
||||
<div className="fixed inset-0 z-50 bg-slate-900/50 backdrop-blur-xs flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-2xl max-w-lg w-full p-6 shadow-2xl space-y-4">
|
||||
<h3 className="text-lg font-bold text-slate-900">Add Memory Record</h3>
|
||||
|
||||
<form onSubmit={handleAddMemory} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-700 mb-1">Key Identifier</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={newKey}
|
||||
onChange={(e) => setNewKey(e.target.value)}
|
||||
placeholder="e.g. user_project_summary"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl text-xs font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-700 mb-1">Value (Text or JSON string)</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
required
|
||||
value={newValue}
|
||||
onChange={(e) => setNewValue(e.target.value)}
|
||||
placeholder='e.g. {"status": "active", "topic": "AI Studio Integration"}'
|
||||
className="w-full p-3 border border-slate-300 rounded-xl text-xs font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-slate-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-700 mb-1">Tags (comma separated)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newTags}
|
||||
onChange={(e) => setNewTags(e.target.value)}
|
||||
placeholder="e.g. config, project"
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-700 mb-1">Agent ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newAgentId}
|
||||
onChange={(e) => setNewAgentId(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddModal(false)}
|
||||
className="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 hover:bg-slate-100"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-semibold shadow-xs"
|
||||
>
|
||||
Save Record
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
205
src/components/OverviewTab.tsx
Normal file
205
src/components/OverviewTab.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import React from 'react';
|
||||
import { Server, Cpu, Activity, Database, Wrench, ArrowRight, Copy, Check, Zap, Globe, Code, ShieldCheck } from 'lucide-react';
|
||||
import { ServerStats } from '../types';
|
||||
|
||||
interface OverviewTabProps {
|
||||
stats: ServerStats | null;
|
||||
setActiveTab: (tab: string) => void;
|
||||
}
|
||||
|
||||
export const OverviewTab: React.FC<OverviewTabProps> = ({ stats, setActiveTab }) => {
|
||||
const [copiedKey, setCopiedKey] = React.useState<string | null>(null);
|
||||
const baseUrl = window.location.origin;
|
||||
|
||||
const copyToClipboard = (text: string, key: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopiedKey(key);
|
||||
setTimeout(() => setCopiedKey(null), 2000);
|
||||
};
|
||||
|
||||
const formatUptime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const hrs = Math.floor(mins / 60);
|
||||
if (hrs > 0) return `${hrs}h ${mins % 60}m`;
|
||||
if (mins > 0) return `${mins}m ${seconds % 60}s`;
|
||||
return `${seconds}s`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Top Banner */}
|
||||
<div className="bg-gradient-to-r from-slate-900 via-indigo-950 to-slate-900 rounded-2xl p-6 sm:p-8 text-white shadow-xl relative overflow-hidden">
|
||||
<div className="absolute -right-10 -bottom-10 w-64 h-64 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||
<div className="relative z-10 max-w-3xl space-y-4">
|
||||
<div className="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-indigo-500/20 border border-indigo-400/30 text-indigo-300 text-xs font-semibold">
|
||||
<Zap className="h-3.5 w-3.5 text-indigo-400" />
|
||||
<span>Ready for Local & Network Agent Integration</span>
|
||||
</div>
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold tracking-tight">
|
||||
Self-Hosted "Do Everything" Tool Suite & MCP Engine
|
||||
</h2>
|
||||
<p className="text-slate-300 text-sm leading-relaxed">
|
||||
Connect Claude Desktop, Cursor, Custom Agents, or LangChain to this server via standard Model Context Protocol (MCP) or REST endpoints. Gives your agent instant access to web scraping, code sandboxing, data conversions, chart generation, memory vault, and network diagnostics.
|
||||
</p>
|
||||
|
||||
<div className="pt-2 flex flex-wrap gap-3">
|
||||
<button
|
||||
onClick={() => setActiveTab('mcp-hub')}
|
||||
className="inline-flex items-center space-x-2 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-medium text-sm transition-all shadow-md shadow-indigo-600/30"
|
||||
>
|
||||
<Cpu className="h-4 w-4" />
|
||||
<span>MCP Server Hub & Store</span>
|
||||
<ArrowRight className="h-4 w-4 ml-1" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('mcp-guide')}
|
||||
className="inline-flex items-center space-x-2 px-4 py-2.5 rounded-xl bg-white/10 hover:bg-white/20 text-white font-medium text-sm transition-all border border-white/10"
|
||||
>
|
||||
<Cpu className="h-4 w-4" />
|
||||
<span>Connect MCP Agent</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('playground')}
|
||||
className="inline-flex items-center space-x-2 px-4 py-2.5 rounded-xl bg-white/10 hover:bg-white/20 text-white font-medium text-sm transition-all border border-white/10"
|
||||
>
|
||||
<Wrench className="h-4 w-4" />
|
||||
<span>Test Tools Suite</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Real-time Metrics Grid */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6">
|
||||
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm space-y-2">
|
||||
<div className="flex items-center justify-between text-slate-500">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider">Server Status</span>
|
||||
<Server className="h-4 w-4 text-emerald-500" />
|
||||
</div>
|
||||
<div className="flex items-baseline space-x-2">
|
||||
<span className="text-xl sm:text-2xl font-bold text-slate-900">Online</span>
|
||||
<span className="text-xs text-slate-500 font-mono">({formatUptime(stats?.uptimeSeconds || 0)})</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">Binds to 0.0.0.0:3000</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm space-y-2">
|
||||
<div className="flex items-center justify-between text-slate-500">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider">MCP Requests</span>
|
||||
<Cpu className="h-4 w-4 text-indigo-500" />
|
||||
</div>
|
||||
<div className="flex items-baseline space-x-2">
|
||||
<span className="text-xl sm:text-2xl font-bold text-slate-900">{stats?.mcpRequests || 0}</span>
|
||||
<span className="text-xs text-slate-500 font-mono">RPC Calls</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">JSON-RPC 2.0 & SSE Stream</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm space-y-2">
|
||||
<div className="flex items-center justify-between text-slate-500">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider">Tool Catalog</span>
|
||||
<Wrench className="h-4 w-4 text-sky-500" />
|
||||
</div>
|
||||
<div className="flex items-baseline space-x-2">
|
||||
<span className="text-xl sm:text-2xl font-bold text-slate-900">{stats?.registeredToolsCount || 19}</span>
|
||||
<span className="text-xs text-slate-500 font-mono">Active Tools</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">Web, Code, Data, Media, Memory</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm space-y-2">
|
||||
<div className="flex items-center justify-between text-slate-500">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider">Memory Vault</span>
|
||||
<Database className="h-4 w-4 text-amber-500" />
|
||||
</div>
|
||||
<div className="flex items-baseline space-x-2">
|
||||
<span className="text-xl sm:text-2xl font-bold text-slate-900">{stats?.activeMemoryItems || 0}</span>
|
||||
<span className="text-xs text-slate-500 font-mono">Records</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">Agent Persistent KV Storage</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Endpoints & Configuration Summary */}
|
||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm space-y-4">
|
||||
<h3 className="text-base font-bold text-slate-900 flex items-center space-x-2">
|
||||
<Globe className="h-5 w-5 text-indigo-600" />
|
||||
<span>Self-Hosted Server Connection Endpoints</span>
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-slate-50 rounded-xl p-4 border border-slate-200 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-slate-700 uppercase tracking-wider">MCP Protocol Endpoint</span>
|
||||
<button
|
||||
onClick={() => copyToClipboard(`${baseUrl}/mcp`, 'mcp')}
|
||||
className="text-xs text-indigo-600 hover:text-indigo-700 font-medium inline-flex items-center space-x-1"
|
||||
>
|
||||
{copiedKey === 'mcp' ? <Check className="h-3.5 w-3.5 text-emerald-600" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
<span>{copiedKey === 'mcp' ? 'Copied' : 'Copy'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<code className="block p-2.5 bg-slate-900 text-indigo-300 rounded-lg text-xs font-mono overflow-x-auto">
|
||||
{baseUrl}/mcp
|
||||
</code>
|
||||
<p className="text-xs text-slate-500">
|
||||
For Claude Desktop, Cursor, and MCP JSON-RPC clients.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 rounded-xl p-4 border border-slate-200 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-slate-700 uppercase tracking-wider">REST API Base URL</span>
|
||||
<button
|
||||
onClick={() => copyToClipboard(`${baseUrl}/api/v1`, 'rest')}
|
||||
className="text-xs text-indigo-600 hover:text-indigo-700 font-medium inline-flex items-center space-x-1"
|
||||
>
|
||||
{copiedKey === 'rest' ? <Check className="h-3.5 w-3.5 text-emerald-600" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
<span>{copiedKey === 'rest' ? 'Copied' : 'Copy'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<code className="block p-2.5 bg-slate-900 text-indigo-300 rounded-lg text-xs font-mono overflow-x-auto">
|
||||
{baseUrl}/api/v1
|
||||
</code>
|
||||
<p className="text-xs text-slate-500">
|
||||
For direct REST tool calls and custom integrations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Architecture & Capability Breakdown */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm space-y-3">
|
||||
<div className="h-10 w-10 rounded-xl bg-blue-100 text-blue-600 flex items-center justify-center">
|
||||
<Code className="h-5 w-5" />
|
||||
</div>
|
||||
<h4 className="text-base font-bold text-slate-900">Web & Code Sandbox</h4>
|
||||
<p className="text-xs text-slate-600 leading-relaxed">
|
||||
Fetch & convert HTML to clean markdown, run web searches, execute sandboxed JavaScript code expressions, evaluate statistical formulas, test regular expressions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm space-y-3">
|
||||
<div className="h-10 w-10 rounded-xl bg-emerald-100 text-emerald-600 flex items-center justify-center">
|
||||
<Database className="h-5 w-5" />
|
||||
</div>
|
||||
<h4 className="text-base font-bold text-slate-900">Data & Media Engines</h4>
|
||||
<p className="text-xs text-slate-600 leading-relaxed">
|
||||
Convert between JSON, CSV, YAML, XML & Query Strings. Generate cryptographic hashes (SHA256, HMAC, UUID), SVG charts, QR codes, and text diffs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm space-y-3">
|
||||
<div className="h-10 w-10 rounded-xl bg-purple-100 text-purple-600 flex items-center justify-center">
|
||||
<ShieldCheck className="h-5 w-5" />
|
||||
</div>
|
||||
<h4 className="text-base font-bold text-slate-900">Agent Long-Term Memory</h4>
|
||||
<p className="text-xs text-slate-600 leading-relaxed">
|
||||
Allow agents to store and search long-term memory records with tags, metadata, and optional TTL expiration for persistent context retention.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
375
src/components/PackageRunnerModal.tsx
Normal file
375
src/components/PackageRunnerModal.tsx
Normal file
@@ -0,0 +1,375 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Layers,
|
||||
XCircle,
|
||||
Terminal,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
RefreshCw,
|
||||
Zap,
|
||||
Sparkles,
|
||||
Download,
|
||||
Plus,
|
||||
Trash2,
|
||||
Box,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface PackageRunnerModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onInstalled: () => void;
|
||||
}
|
||||
|
||||
export const PackageRunnerModal: React.FC<PackageRunnerModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onInstalled,
|
||||
}) => {
|
||||
const [packageManager, setPackageManager] = useState<'bunx' | 'bun' | 'npx' | 'uvx' | 'python' | 'npm'>('bunx');
|
||||
const [packageName, setPackageName] = useState<string>('@modelcontextprotocol/server-filesystem');
|
||||
const [serverName, setServerName] = useState<string>('Filesystem Explorer MCP');
|
||||
const [category, setCategory] = useState<'dev' | 'data' | 'code' | 'web' | 'media'>('dev');
|
||||
const [argsList, setArgsList] = useState<string>('/workspace');
|
||||
const [envVars, setEnvVars] = useState<Array<{ id: string; key: string; value: string }>>([]);
|
||||
const [isInstalling, setIsInstalling] = useState<boolean>(false);
|
||||
const [installResult, setInstallResult] = useState<{ success?: boolean; message?: string } | null>(null);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const popularPackages = [
|
||||
{
|
||||
name: '@modelcontextprotocol/server-filesystem',
|
||||
pm: 'bunx' as const,
|
||||
category: 'dev' as const,
|
||||
args: '/workspace',
|
||||
title: 'Filesystem Explorer MCP',
|
||||
desc: 'Access workspace files, directories, and project source code.',
|
||||
},
|
||||
{
|
||||
name: 'mcp-server-git',
|
||||
pm: 'uvx' as const,
|
||||
category: 'code' as const,
|
||||
args: '--repository .',
|
||||
title: 'Git Repository & History MCP',
|
||||
desc: 'Inspect git commits, branches, staged diffs, and history logs.',
|
||||
},
|
||||
{
|
||||
name: 'mcp-server-sqlite',
|
||||
pm: 'uvx' as const,
|
||||
category: 'data' as const,
|
||||
args: '--db-path /workspace/data.db',
|
||||
title: 'SQLite Database MCP',
|
||||
desc: 'Execute queries, table schemas, and relational SQLite operations.',
|
||||
},
|
||||
{
|
||||
name: '@modelcontextprotocol/server-memory',
|
||||
pm: 'npx' as const,
|
||||
category: 'data' as const,
|
||||
args: '',
|
||||
title: 'Knowledge Graph Memory MCP',
|
||||
desc: 'Entity and relation-based persistent knowledge graph memory.',
|
||||
},
|
||||
{
|
||||
name: 'mcp-server-fetch',
|
||||
pm: 'uvx' as const,
|
||||
category: 'web' as const,
|
||||
args: '',
|
||||
title: 'Web Fetch & HTML to Markdown MCP',
|
||||
desc: 'Scrapes web pages and converts dynamic HTML to Markdown.',
|
||||
},
|
||||
{
|
||||
name: 'mcp-server-brave-search',
|
||||
pm: 'npx' as const,
|
||||
category: 'web' as const,
|
||||
args: '',
|
||||
title: 'Brave Search Engine MCP',
|
||||
desc: 'Real-time live web indexing and search result extraction.',
|
||||
},
|
||||
];
|
||||
|
||||
const handleSelectPopular = (pkg: typeof popularPackages[0]) => {
|
||||
setPackageManager(pkg.pm);
|
||||
setPackageName(pkg.name);
|
||||
setServerName(pkg.title);
|
||||
setCategory(pkg.category);
|
||||
setArgsList(pkg.args);
|
||||
};
|
||||
|
||||
const handleAddEnv = () => {
|
||||
setEnvVars((prev) => [...prev, { id: `env-${Date.now()}`, key: '', value: '' }]);
|
||||
};
|
||||
|
||||
const handleRemoveEnv = (id: string) => {
|
||||
setEnvVars((prev) => prev.filter((e) => e.id !== id));
|
||||
};
|
||||
|
||||
const handleUpdateEnv = (id: string, field: 'key' | 'value', val: string) => {
|
||||
setEnvVars((prev) =>
|
||||
prev.map((e) => (e.id === id ? { ...e, [field]: val } : e))
|
||||
);
|
||||
};
|
||||
|
||||
const handleInstall = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!packageName.trim()) return;
|
||||
|
||||
setIsInstalling(true);
|
||||
setInstallResult(null);
|
||||
|
||||
try {
|
||||
const customEnvMap: Record<string, string> = {};
|
||||
envVars.forEach((e) => {
|
||||
if (e.key.trim()) customEnvMap[e.key.trim()] = e.value;
|
||||
});
|
||||
|
||||
const parsedArgs = argsList.trim() ? argsList.split(/\s+/).filter(Boolean) : [];
|
||||
|
||||
const res = await fetch('/api/v1/mcp/install-package', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
packageManager,
|
||||
packageName: packageName.trim(),
|
||||
serverName: serverName.trim() || `MCP (${packageManager} ${packageName})`,
|
||||
category,
|
||||
args: parsedArgs,
|
||||
customEnv: customEnvMap,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setInstallResult({
|
||||
success: true,
|
||||
message: data.message || `Successfully installed ${packageName}!`,
|
||||
});
|
||||
setTimeout(() => {
|
||||
onInstalled();
|
||||
onClose();
|
||||
}, 1200);
|
||||
} else {
|
||||
setInstallResult({
|
||||
success: false,
|
||||
message: data.error || 'Failed to install MCP package.',
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
setInstallResult({
|
||||
success: false,
|
||||
message: err.message || 'Error executing package installer.',
|
||||
});
|
||||
} finally {
|
||||
setIsInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-slate-900/70 backdrop-blur-xs flex items-center justify-center p-4 overflow-y-auto">
|
||||
<div className="bg-white rounded-3xl max-w-2xl w-full p-6 sm:p-8 space-y-6 shadow-2xl border border-slate-200 my-8">
|
||||
<div className="flex items-start justify-between border-b border-slate-100 pb-5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 rounded-2xl bg-indigo-600 text-white flex items-center justify-center shadow-md">
|
||||
<Download className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-wider bg-indigo-100 text-indigo-800 border border-indigo-200">
|
||||
BROWSER RUNNER & INSTALLER
|
||||
</span>
|
||||
<span className="text-xs text-slate-500 font-medium">bun / bunx / npx / uvx / python / npm</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-slate-900 mt-0.5">
|
||||
Install Any MCP Server from Browser
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-xl transition"
|
||||
>
|
||||
<XCircle className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 1-Click Community Preset Carousel */}
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase tracking-wider text-slate-500 mb-2">
|
||||
1-Click Popular MCP Packages
|
||||
</label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{popularPackages.map((pkg) => (
|
||||
<button
|
||||
key={pkg.name}
|
||||
type="button"
|
||||
onClick={() => handleSelectPopular(pkg)}
|
||||
className={`p-2.5 rounded-xl border text-left transition ${
|
||||
packageName === pkg.name
|
||||
? 'border-indigo-600 bg-indigo-50/80 text-indigo-900 shadow-xs'
|
||||
: 'border-slate-200 bg-slate-50 hover:bg-white text-slate-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between text-[10px] font-bold text-indigo-600 mb-1">
|
||||
<span>{pkg.pm}</span>
|
||||
<span className="text-slate-400 font-mono">{pkg.category}</span>
|
||||
</div>
|
||||
<div className="text-xs font-bold truncate text-slate-900">{pkg.title}</div>
|
||||
<div className="text-[10px] text-slate-500 truncate mt-0.5">{pkg.desc}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleInstall} className="space-y-4 text-xs sm:text-sm">
|
||||
{/* Package Manager & Package Name */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block font-bold text-slate-800 mb-1">Package Runner</label>
|
||||
<select
|
||||
value={packageManager}
|
||||
onChange={(e) => setPackageManager(e.target.value as any)}
|
||||
className="w-full px-3 py-2.5 bg-slate-50 border border-slate-300 rounded-xl font-bold text-slate-800 focus:ring-2 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="bunx">⚡ bunx (Fastest)</option>
|
||||
<option value="bun">🥟 bun</option>
|
||||
<option value="npx">📦 npx (Node.js)</option>
|
||||
<option value="uvx">🐍 uvx (Python UV)</option>
|
||||
<option value="python">🐍 python / pip</option>
|
||||
<option value="npm">📦 npm</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2">
|
||||
<label className="block font-bold text-slate-800 mb-1">
|
||||
Package Name or Command
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={packageName}
|
||||
onChange={(e) => setPackageName(e.target.value)}
|
||||
placeholder="e.g. @modelcontextprotocol/server-filesystem"
|
||||
className="w-full px-3 py-2.5 bg-slate-50 border border-slate-300 rounded-xl font-mono text-xs focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Server Title & Category */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block font-bold text-slate-800 mb-1">MCP Server Display Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={serverName}
|
||||
onChange={(e) => setServerName(e.target.value)}
|
||||
placeholder="e.g. Filesystem Explorer MCP"
|
||||
className="w-full px-3 py-2.5 bg-slate-50 border border-slate-300 rounded-xl focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block font-bold text-slate-800 mb-1">Tool Category</label>
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value as any)}
|
||||
className="w-full px-3 py-2.5 bg-slate-50 border border-slate-300 rounded-xl font-medium focus:ring-2 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="dev">Developer & System Tools</option>
|
||||
<option value="code">Code & Syntax Tools</option>
|
||||
<option value="data">Data & Databases</option>
|
||||
<option value="web">Web & Scrapers</option>
|
||||
<option value="media">Media & Visuals</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Command Line Arguments */}
|
||||
<div>
|
||||
<label className="block font-bold text-slate-800 mb-1">
|
||||
CLI Arguments & Flags (Space-separated)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={argsList}
|
||||
onChange={(e) => setArgsList(e.target.value)}
|
||||
placeholder="e.g. /workspace --readonly or --db-path /data.db"
|
||||
className="w-full px-3 py-2.5 bg-slate-50 border border-slate-300 rounded-xl font-mono text-xs focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Environment Variables */}
|
||||
<div className="bg-slate-50 p-4 rounded-2xl border border-slate-200 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold text-slate-800 text-xs">Custom Environment Variables (Optional)</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddEnv}
|
||||
className="px-2 py-1 text-[11px] font-semibold bg-white hover:bg-slate-100 text-indigo-600 border border-slate-300 rounded-lg flex items-center space-x-1"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
<span>Add Variable</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{envVars.map((env) => (
|
||||
<div key={env.id} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={env.key}
|
||||
onChange={(e) => handleUpdateEnv(env.id, 'key', e.target.value)}
|
||||
placeholder="ENV_NAME"
|
||||
className="w-1/2 px-3 py-1.5 bg-white border border-slate-300 rounded-lg font-mono text-xs uppercase"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={env.value}
|
||||
onChange={(e) => handleUpdateEnv(env.id, 'value', e.target.value)}
|
||||
placeholder="value"
|
||||
className="w-1/2 px-3 py-1.5 bg-white border border-slate-300 rounded-lg font-mono text-xs"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveEnv(env.id)}
|
||||
className="p-1.5 text-slate-400 hover:text-red-500"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{installResult && (
|
||||
<div
|
||||
className={`p-3 rounded-xl flex items-center space-x-2 text-xs ${
|
||||
installResult.success
|
||||
? 'bg-emerald-50 text-emerald-800 border border-emerald-200'
|
||||
: 'bg-red-50 text-red-800 border border-red-200'
|
||||
}`}
|
||||
>
|
||||
{installResult.success ? <CheckCircle2 className="w-4 h-4 text-emerald-600" /> : <AlertCircle className="w-4 h-4 text-red-600" />}
|
||||
<span>{installResult.message}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end space-x-3 pt-3 border-t border-slate-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-5 py-2.5 bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold rounded-xl transition"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isInstalling}
|
||||
className="px-6 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white font-bold rounded-xl shadow-md transition flex items-center space-x-2"
|
||||
>
|
||||
{isInstalling ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Zap className="w-4 h-4" />}
|
||||
<span>{isInstalling ? 'Installing MCP Package...' : 'Install & Enable in Hub'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
432
src/components/RoadSignModal.tsx
Normal file
432
src/components/RoadSignModal.tsx
Normal file
@@ -0,0 +1,432 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Compass,
|
||||
XCircle,
|
||||
Key,
|
||||
Plus,
|
||||
Trash2,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
RefreshCw,
|
||||
Globe,
|
||||
Shield,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Radio,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { MCPServerDefinition } from '../types';
|
||||
|
||||
interface RoadSignModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSaveRoadSign: (server: Partial<MCPServerDefinition>) => Promise<void>;
|
||||
existingServer?: MCPServerDefinition | null;
|
||||
}
|
||||
|
||||
export const RoadSignModal: React.FC<RoadSignModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSaveRoadSign,
|
||||
existingServer,
|
||||
}) => {
|
||||
const [signpostTitle, setSignpostTitle] = useState<string>(
|
||||
existingServer?.roadSign?.signpostTitle || 'LOOK HERE: Windows Workstation Desktop MCP'
|
||||
);
|
||||
const [serverName, setServerName] = useState<string>(
|
||||
existingServer?.name || 'Windows Desktop Automation MCP'
|
||||
);
|
||||
const [targetHostLocation, setTargetHostLocation] = useState<string>(
|
||||
existingServer?.roadSign?.targetHostLocation || existingServer?.endpointUrl || 'http://192.168.1.150:8000/mcp'
|
||||
);
|
||||
const [osPlatform, setOsPlatform] = useState<'windows' | 'macos' | 'linux' | 'cloud' | 'docker'>(
|
||||
existingServer?.roadSign?.osPlatform || 'windows'
|
||||
);
|
||||
const [directionsInstructions, setDirectionsInstructions] = useState<string>(
|
||||
existingServer?.roadSign?.directionsInstructions ||
|
||||
'Directs AI models and sub-agents to access local Windows workstation tools (PowerShell CLI, registry access, desktop automation) with injected Bearer token credentials.'
|
||||
);
|
||||
|
||||
// Key-value credentials map
|
||||
const [credentialsList, setCredentialsList] = useState<Array<{ id: string; key: string; value: string; isSecret: boolean }>>(() => {
|
||||
const existingKeys = existingServer?.roadSign?.credentialKeys || existingServer?.headers || {};
|
||||
const entries = Object.entries(existingKeys);
|
||||
if (entries.length > 0) {
|
||||
return entries.map(([key, value], idx) => ({
|
||||
id: `cred-${idx}`,
|
||||
key,
|
||||
value: String(value),
|
||||
isSecret: true,
|
||||
}));
|
||||
}
|
||||
return [
|
||||
{ id: 'cred-1', key: 'Authorization', value: 'Bearer win_mcp_sec_9941a8', isSecret: true },
|
||||
{ id: 'cred-2', key: 'X-Client-Origin', value: 'DoEverythingMCP-Nexus', isSecret: false },
|
||||
];
|
||||
});
|
||||
|
||||
const [revealedSecrets, setRevealedSecrets] = useState<Record<string, boolean>>({});
|
||||
const [isTesting, setIsTesting] = useState<boolean>(false);
|
||||
const [testStatus, setTestStatus] = useState<{ success?: boolean; message?: string; latency?: number } | null>(null);
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleAddCredential = () => {
|
||||
setCredentialsList((prev) => [
|
||||
...prev,
|
||||
{ id: `cred-${Date.now()}`, key: '', value: '', isSecret: true },
|
||||
]);
|
||||
};
|
||||
|
||||
const handleRemoveCredential = (id: string) => {
|
||||
setCredentialsList((prev) => prev.filter((c) => c.id !== id));
|
||||
};
|
||||
|
||||
const handleUpdateCredential = (id: string, field: 'key' | 'value', val: string) => {
|
||||
setCredentialsList((prev) =>
|
||||
prev.map((c) => (c.id === id ? { ...c, [field]: val } : c))
|
||||
);
|
||||
};
|
||||
|
||||
const toggleReveal = (id: string) => {
|
||||
setRevealedSecrets((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
setIsTesting(true);
|
||||
setTestStatus(null);
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const headersMap: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
credentialsList.forEach((c) => {
|
||||
if (c.key.trim()) headersMap[c.key.trim()] = c.value;
|
||||
});
|
||||
|
||||
const res = await fetch(targetHostLocation, {
|
||||
method: 'POST',
|
||||
headers: headersMap,
|
||||
body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method: 'ping' }),
|
||||
});
|
||||
|
||||
const latency = Date.now() - startTime;
|
||||
if (res.ok) {
|
||||
setTestStatus({
|
||||
success: true,
|
||||
latency,
|
||||
message: `Remote Signpost responded 200 OK (${latency}ms). Credentials valid!`,
|
||||
});
|
||||
} else {
|
||||
setTestStatus({
|
||||
success: false,
|
||||
latency,
|
||||
message: `Remote server responded with HTTP ${res.status}: ${res.statusText}`,
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
setTestStatus({
|
||||
success: false,
|
||||
latency: Date.now() - startTime,
|
||||
message: `Connection check failed: ${err.message || 'Network unreachable'}. Please verify host address and firewall rules.`,
|
||||
});
|
||||
} finally {
|
||||
setIsTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!targetHostLocation.trim()) {
|
||||
alert('Target Host Location URL is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const credentialKeysMap: Record<string, string> = {};
|
||||
credentialsList.forEach((c) => {
|
||||
if (c.key.trim()) {
|
||||
credentialKeysMap[c.key.trim()] = c.value;
|
||||
}
|
||||
});
|
||||
|
||||
const roadSignPayload: Partial<MCPServerDefinition> = {
|
||||
id: existingServer?.id,
|
||||
name: serverName,
|
||||
description: directionsInstructions,
|
||||
type: 'remote_http',
|
||||
category: 'dev',
|
||||
enabled: true,
|
||||
endpointUrl: targetHostLocation,
|
||||
headers: credentialKeysMap,
|
||||
roadSign: {
|
||||
isRoadSign: true,
|
||||
signpostTitle: signpostTitle.trim(),
|
||||
targetHostLocation: targetHostLocation.trim(),
|
||||
directionsInstructions: directionsInstructions.trim(),
|
||||
credentialKeys: credentialKeysMap,
|
||||
osPlatform,
|
||||
},
|
||||
tools: existingServer?.tools && existingServer.tools.length > 0 ? existingServer.tools : [
|
||||
{
|
||||
name: `${osPlatform}_remote_command_exec`,
|
||||
description: `Execute commands on remote ${osPlatform} machine (${signpostTitle})`,
|
||||
category: 'dev',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
command: { type: 'string', description: 'Command to execute on the remote machine' },
|
||||
},
|
||||
required: ['command'],
|
||||
},
|
||||
},
|
||||
],
|
||||
healthStatus: 'healthy',
|
||||
lastPingAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await onSaveRoadSign(roadSignPayload);
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
alert('Failed to save Road Sign: ' + err.message);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-slate-900/70 backdrop-blur-xs flex items-center justify-center p-4 overflow-y-auto">
|
||||
<div className="bg-white rounded-3xl max-w-2xl w-full p-6 sm:p-8 space-y-6 shadow-2xl border border-slate-200 my-8">
|
||||
{/* Header with Highway Sign Metaphor */}
|
||||
<div className="flex items-start justify-between border-b border-slate-100 pb-5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 rounded-2xl bg-amber-500 text-slate-950 font-black flex items-center justify-center shadow-md border-2 border-amber-400">
|
||||
<Compass className="w-6 h-6 animate-pulse" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-black uppercase tracking-wider bg-amber-100 text-amber-900 border border-amber-300">
|
||||
ROAD SIGN SIGNPOST
|
||||
</span>
|
||||
<span className="text-xs text-slate-500 font-medium">Remote Host Bridge</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-slate-900 mt-0.5">
|
||||
{existingServer ? 'Edit Remote MCP Road Sign' : 'Erect New MCP Road Sign & Signpost'}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-xl transition"
|
||||
>
|
||||
<XCircle className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Visual Road Sign Metaphor Preview */}
|
||||
<div className="bg-gradient-to-r from-amber-500 via-amber-400 to-yellow-500 rounded-2xl p-4 text-slate-950 shadow-inner border-2 border-amber-600">
|
||||
<div className="flex items-center justify-between text-xs font-black tracking-widest uppercase mb-1 opacity-80">
|
||||
<span>HIGHWAY SIGNPOST • MCP NEXUS ROUTE</span>
|
||||
<span>OS: {osPlatform.toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="text-base sm:text-lg font-black tracking-tight text-slate-950 flex items-center space-x-2">
|
||||
<span>👉</span>
|
||||
<span>{signpostTitle || 'LOOK HERE: Remote Host MCP'}</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-900 font-mono mt-1 bg-amber-200/80 px-2 py-1 rounded inline-block">
|
||||
Target Host: {targetHostLocation || 'http://[remote-ip]:8000/mcp'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5 text-xs sm:text-sm">
|
||||
{/* Signpost Title & Host OS */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div className="sm:col-span-2">
|
||||
<label className="block font-bold text-slate-800 mb-1">
|
||||
Road Sign Title (e.g., "LOOK HERE: Windows Workstation MCP")
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={signpostTitle}
|
||||
onChange={(e) => setSignpostTitle(e.target.value)}
|
||||
placeholder="LOOK HERE: Windows Workstation Desktop MCP"
|
||||
className="w-full px-3.5 py-2.5 bg-slate-50 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 font-semibold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block font-bold text-slate-800 mb-1">Remote OS Platform</label>
|
||||
<select
|
||||
value={osPlatform}
|
||||
onChange={(e) => setOsPlatform(e.target.value as any)}
|
||||
className="w-full px-3.5 py-2.5 bg-slate-50 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 font-semibold"
|
||||
>
|
||||
<option value="windows">🪟 Windows</option>
|
||||
<option value="macos">🍎 macOS</option>
|
||||
<option value="linux">🐧 Linux</option>
|
||||
<option value="cloud">☁️ Cloud Node</option>
|
||||
<option value="docker">🐳 Docker Host</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remote Host URL & Server Name */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block font-bold text-slate-800 mb-1">MCP Server Registered Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={serverName}
|
||||
onChange={(e) => setServerName(e.target.value)}
|
||||
placeholder="e.g. Windows Desktop Automation MCP"
|
||||
className="w-full px-3.5 py-2.5 bg-slate-50 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block font-bold text-slate-800 mb-1">
|
||||
Remote Host Endpoint URL
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
required
|
||||
value={targetHostLocation}
|
||||
onChange={(e) => setTargetHostLocation(e.target.value)}
|
||||
placeholder="http://192.168.1.150:8000/mcp or https://mcp.domain.com/mcp"
|
||||
className="w-full px-3.5 py-2.5 bg-slate-50 border border-slate-300 rounded-xl font-mono text-xs focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Road Sign Instructions / Guidance */}
|
||||
<div>
|
||||
<label className="block font-bold text-slate-800 mb-1">
|
||||
Directions & Access Instructions for Agents
|
||||
</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
value={directionsInstructions}
|
||||
onChange={(e) => setDirectionsInstructions(e.target.value)}
|
||||
placeholder="Tell agents how to utilize this remote signpost and what tools it exposes..."
|
||||
className="w-full px-3.5 py-2.5 bg-slate-50 border border-slate-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Interactive Credential & API Key Manager */}
|
||||
<div className="bg-slate-900 text-white p-4 sm:p-5 rounded-2xl space-y-4 border border-slate-800 shadow-md">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Key className="w-4 h-4 text-amber-400" />
|
||||
<span className="font-bold text-sm text-slate-100">Remote API Keys & Header Credentials</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddCredential}
|
||||
className="px-2.5 py-1 bg-amber-500/20 text-amber-300 hover:bg-amber-500/30 border border-amber-500/40 rounded-lg text-xs font-semibold flex items-center space-x-1 transition"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>Add Header / Key</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-400">
|
||||
Credentials configured here will be automatically injected into HTTP headers whenever an AI agent routes a tool call to this signpost.
|
||||
</p>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
{credentialsList.map((cred) => {
|
||||
const isRevealed = revealedSecrets[cred.id];
|
||||
return (
|
||||
<div key={cred.id} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={cred.key}
|
||||
onChange={(e) => handleUpdateCredential(cred.id, 'key', e.target.value)}
|
||||
placeholder="Header Name (e.g. Authorization or X-API-Key)"
|
||||
className="w-1/3 px-3 py-2 bg-slate-800 text-amber-300 rounded-xl border border-slate-700 text-xs font-mono focus:outline-none focus:ring-1 focus:ring-amber-400"
|
||||
/>
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
type={isRevealed ? 'text' : 'password'}
|
||||
value={cred.value}
|
||||
onChange={(e) => handleUpdateCredential(cred.id, 'value', e.target.value)}
|
||||
placeholder="Token / Secret Value (e.g. Bearer win_mcp_sec_9941a8)"
|
||||
className="w-full px-3 py-2 pr-9 bg-slate-800 text-slate-200 rounded-xl border border-slate-700 text-xs font-mono focus:outline-none focus:ring-1 focus:ring-amber-400"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleReveal(cred.id)}
|
||||
className="absolute right-2.5 top-2.5 text-slate-400 hover:text-slate-200"
|
||||
>
|
||||
{isRevealed ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
{credentialsList.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveCredential(cred.id)}
|
||||
className="p-2 text-slate-400 hover:text-red-400 transition"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Test Connection Button */}
|
||||
<div className="pt-2 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 border-t border-slate-800">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTestConnection}
|
||||
disabled={isTesting}
|
||||
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-amber-300 font-semibold rounded-xl text-xs flex items-center space-x-2 border border-slate-700 transition"
|
||||
>
|
||||
{isTesting ? <RefreshCw className="w-3.5 h-3.5 animate-spin" /> : <Radio className="w-3.5 h-3.5" />}
|
||||
<span>{isTesting ? 'Pinging Remote Host...' : 'Ping Remote Road Sign'}</span>
|
||||
</button>
|
||||
|
||||
{testStatus && (
|
||||
<div
|
||||
className={`text-xs px-3 py-1.5 rounded-lg flex items-center space-x-1.5 ${
|
||||
testStatus.success
|
||||
? 'bg-emerald-900/50 text-emerald-300 border border-emerald-700/50'
|
||||
: 'bg-red-900/50 text-red-300 border border-red-700/50'
|
||||
}`}
|
||||
>
|
||||
{testStatus.success ? <CheckCircle2 className="w-3.5 h-3.5" /> : <AlertCircle className="w-3.5 h-3.5" />}
|
||||
<span>{testStatus.message}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-end space-x-3 pt-4 border-t border-slate-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-5 py-2.5 bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold rounded-xl transition"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
className="px-6 py-2.5 bg-amber-500 hover:bg-amber-600 text-slate-950 font-black rounded-xl shadow-md transition flex items-center space-x-2"
|
||||
>
|
||||
{isSaving ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Compass className="w-4 h-4" />}
|
||||
<span>{isSaving ? 'Saving Signpost...' : 'Save & Publish Road Sign'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
259
src/components/SecurityGatewayTab.tsx
Normal file
259
src/components/SecurityGatewayTab.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ShieldCheck, Key, Lock, Unlock, Plus, Trash2, Copy, Check, AlertCircle, RefreshCw, Layers } from 'lucide-react';
|
||||
|
||||
interface AgentKey {
|
||||
id: string;
|
||||
name: string;
|
||||
key: string;
|
||||
scope: 'full' | 'read_only' | 'restricted';
|
||||
rateLimitPerMin: number;
|
||||
requestCount: number;
|
||||
lastUsedAt?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export const SecurityGatewayTab: React.FC = () => {
|
||||
const [authRequired, setAuthRequired] = useState(false);
|
||||
const [keys, setKeys] = useState<AgentKey[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [newKeyName, setNewKeyName] = useState('');
|
||||
const [newKeyScope, setNewKeyScope] = useState<'full' | 'read_only'>('full');
|
||||
const [newRateLimit, setNewRateLimit] = useState(120);
|
||||
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||
|
||||
const fetchSecurityState = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/v1/mcp/security/keys');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setAuthRequired(data.authRequired);
|
||||
setKeys(data.keys || []);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSecurityState();
|
||||
}, []);
|
||||
|
||||
const handleToggleAuth = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/v1/mcp/security/toggle-auth', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ required: !authRequired }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setAuthRequired(data.authRequired);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateKey = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newKeyName.trim()) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/v1/mcp/security/keys', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: newKeyName,
|
||||
scope: newKeyScope,
|
||||
rateLimitPerMin: newRateLimit,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
setNewKeyName('');
|
||||
fetchSecurityState();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevokeKey = async (id: string) => {
|
||||
if (!confirm('Are you sure you want to revoke this Agent Access Key?')) return;
|
||||
try {
|
||||
await fetch(`/api/v1/mcp/security/keys/${id}`, { method: 'DELETE' });
|
||||
fetchSecurityState();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const copyKey = (key: string) => {
|
||||
navigator.clipboard.writeText(key);
|
||||
setCopiedKey(key);
|
||||
setTimeout(() => setCopiedKey(null), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header Banner */}
|
||||
<div className="bg-gradient-to-r from-slate-900 via-sky-950 to-slate-900 rounded-2xl p-6 sm:p-8 text-white shadow-xl relative overflow-hidden">
|
||||
<div className="relative z-10 max-w-3xl">
|
||||
<span className="inline-flex items-center space-x-1.5 px-3 py-1 rounded-full text-xs font-semibold bg-sky-500/20 text-sky-300 border border-sky-500/30 mb-3">
|
||||
<ShieldCheck className="h-3.5 w-3.5" />
|
||||
<span>MCP Security Gateway</span>
|
||||
</span>
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold tracking-tight">Agent Authentication & Security Gateway</h2>
|
||||
<p className="mt-2 text-slate-300 text-sm leading-relaxed">
|
||||
Manage Bearer tokens and access security policies for AI agents connecting to your self-hosted MCP Hub. Enforce rate limiting per agent and restrict permissions to protect sensitive internal tools.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auth Toggle Banner */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`p-3 rounded-2xl ${authRequired ? 'bg-amber-100 text-amber-700' : 'bg-emerald-100 text-emerald-700'}`}>
|
||||
{authRequired ? <Lock className="h-6 w-6" /> : <Unlock className="h-6 w-6" />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="text-base font-bold text-slate-900">Security Enforcement Mode</h3>
|
||||
<span className={`px-2.5 py-0.5 rounded-full text-xs font-semibold ${authRequired ? 'bg-amber-100 text-amber-800' : 'bg-emerald-100 text-emerald-800'}`}>
|
||||
{authRequired ? 'STRICT (Bearer Key Required)' : 'OPEN LOCAL ACCESS'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
{authRequired
|
||||
? 'All incoming requests to /mcp must supply a valid Authorization: Bearer <mcp_live_sec_key_...> header.'
|
||||
: 'Local and internal requests to /mcp are accepted without token header check.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleToggleAuth}
|
||||
className={`px-5 py-2.5 rounded-xl font-semibold text-xs transition-all shadow-md shrink-0 ${
|
||||
authRequired
|
||||
? 'bg-slate-200 text-slate-800 hover:bg-slate-300'
|
||||
: 'bg-indigo-600 hover:bg-indigo-500 text-white shadow-indigo-600/30'
|
||||
}`}
|
||||
>
|
||||
{authRequired ? 'Disable Enforcement' : 'Enable Bearer Token Enforcement'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Keys Management */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Create Key Form */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm space-y-4">
|
||||
<h3 className="text-base font-bold text-slate-900 flex items-center space-x-2">
|
||||
<Key className="h-5 w-5 text-indigo-600" />
|
||||
<span>Issue Agent Key</span>
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleCreateKey} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-700 mb-1">Agent Name / Identifier:</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
placeholder="e.g. Cursor IDE Prod Agent"
|
||||
className="w-full rounded-lg border border-slate-300 p-2.5 text-xs text-slate-900 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-700 mb-1">Permission Scope:</label>
|
||||
<select
|
||||
value={newKeyScope}
|
||||
onChange={(e: any) => setNewKeyScope(e.target.value)}
|
||||
className="w-full rounded-lg border border-slate-300 p-2.5 text-xs text-slate-900 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="full">Full Access (Read & Execute All Tools)</option>
|
||||
<option value="read_only">Read-Only (tools/list & prompts/list)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-700 mb-1">Rate Limit (req/min):</label>
|
||||
<input
|
||||
type="number"
|
||||
value={newRateLimit}
|
||||
onChange={(e) => setNewRateLimit(Number(e.target.value))}
|
||||
className="w-full rounded-lg border border-slate-300 p-2.5 text-xs text-slate-900 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-2.5 px-4 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-medium text-xs transition-all shadow-md shadow-indigo-600/20 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Generate Agent Key</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Keys List */}
|
||||
<div className="lg:col-span-2 bg-white rounded-2xl border border-slate-200 p-6 shadow-sm space-y-4">
|
||||
<h3 className="text-base font-bold text-slate-900">Active Agent Access Keys</h3>
|
||||
|
||||
{loading ? (
|
||||
<div className="p-8 text-center text-slate-400">
|
||||
<RefreshCw className="h-5 w-5 animate-spin mx-auto mb-2 text-indigo-600" />
|
||||
<span className="text-xs">Loading keys...</span>
|
||||
</div>
|
||||
) : keys.length === 0 ? (
|
||||
<div className="p-8 text-center text-slate-400 text-xs">No keys generated yet</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{keys.map((k) => (
|
||||
<div key={k.id} className="p-4 rounded-xl border border-slate-200 bg-slate-50 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-bold text-slate-900 text-sm">{k.name}</span>
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-indigo-100 text-indigo-800">
|
||||
{k.scope}
|
||||
</span>
|
||||
<span className="text-xs text-slate-500 font-mono">({k.rateLimitPerMin} req/m)</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<code className="text-xs font-mono bg-slate-900 text-emerald-400 px-2.5 py-1 rounded-md">
|
||||
{k.key}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copyKey(k.key)}
|
||||
className="p-1 rounded hover:bg-slate-200 text-slate-600 transition-colors"
|
||||
title="Copy Key"
|
||||
>
|
||||
{copiedKey === k.key ? <Check className="h-4 w-4 text-emerald-600" /> : <Copy className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-[10px] text-slate-500">
|
||||
Requests handled: <strong className="text-slate-800">{k.requestCount}</strong> • Last used: {k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleTimeString() : 'Never'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleRevokeKey(k.id)}
|
||||
className="p-2 rounded-lg text-red-600 hover:bg-red-50 hover:text-red-700 transition-colors self-start sm:self-center"
|
||||
title="Revoke Key"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
174
src/components/SettingsTab.tsx
Normal file
174
src/components/SettingsTab.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React from 'react';
|
||||
import { Settings, ShieldCheck, Key, FileCode, Copy, Check, Download, ExternalLink, Server } from 'lucide-react';
|
||||
import { ServerStats } from '../types';
|
||||
|
||||
interface SettingsTabProps {
|
||||
stats: ServerStats | null;
|
||||
}
|
||||
|
||||
export const SettingsTab: React.FC<SettingsTabProps> = ({ stats }) => {
|
||||
const [openApiSpec, setOpenApiSpec] = React.useState<any>(null);
|
||||
const [copiedSpec, setCopiedSpec] = React.useState<boolean>(false);
|
||||
const [loadingSpec, setLoadingSpec] = React.useState<boolean>(false);
|
||||
|
||||
const fetchOpenApi = async () => {
|
||||
setLoadingSpec(true);
|
||||
try {
|
||||
const res = await fetch('/openapi.json');
|
||||
const data = await res.json();
|
||||
setOpenApiSpec(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch OpenAPI spec:', err);
|
||||
} finally {
|
||||
setLoadingSpec(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchOpenApi();
|
||||
}, []);
|
||||
|
||||
const downloadOpenApiJSON = () => {
|
||||
if (!openApiSpec) return;
|
||||
const jsonStr = JSON.stringify(openApiSpec, null, 2);
|
||||
const blob = new Blob([jsonStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'do_everything_api_openapi_v3.json';
|
||||
a.click();
|
||||
};
|
||||
|
||||
const copySpec = () => {
|
||||
if (!openApiSpec) return;
|
||||
navigator.clipboard.writeText(JSON.stringify(openApiSpec, null, 2));
|
||||
setCopiedSpec(true);
|
||||
setTimeout(() => setCopiedSpec(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm space-y-2">
|
||||
<h2 className="text-xl font-bold text-slate-900 flex items-center space-x-2">
|
||||
<Settings className="h-6 w-6 text-indigo-600" />
|
||||
<span>Self-Hosting Configuration & OpenAPI Specification</span>
|
||||
</h2>
|
||||
<p className="text-xs text-slate-600">
|
||||
Manage system runtime configuration, security environment variables, and export OpenAPI 3.0 schemas.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Grid: Environment Check & Server Params */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Environment Credentials */}
|
||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm space-y-4">
|
||||
<h3 className="text-base font-bold text-slate-900 flex items-center space-x-2">
|
||||
<Key className="h-5 w-5 text-amber-500" />
|
||||
<span>AI Model & API Secrets Status</span>
|
||||
</h3>
|
||||
|
||||
<div className="p-4 bg-slate-50 border border-slate-200 rounded-xl space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-slate-700">GEMINI_API_KEY</span>
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold ${
|
||||
stats?.hasGeminiKey ? 'bg-emerald-100 text-emerald-800' : 'bg-amber-100 text-amber-800'
|
||||
}`}
|
||||
>
|
||||
{stats?.hasGeminiKey ? 'ACTIVE & CONFIGURED' : 'USING HEURISTIC FALLBACK'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 leading-relaxed">
|
||||
Required for web search synthesis, AI document summarization, and LLM classification. AI Studio injects this automatically from the Secrets panel.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-slate-50 border border-slate-200 rounded-xl space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-slate-700">NETWORK_PORT & BINDING</span>
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-indigo-100 text-indigo-800">
|
||||
0.0.0.0 : 3000
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 leading-relaxed">
|
||||
Bound to all network interfaces for local network self-hosting and container reverse proxy ingress.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security & Access */}
|
||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm space-y-4">
|
||||
<h3 className="text-base font-bold text-slate-900 flex items-center space-x-2">
|
||||
<ShieldCheck className="h-5 w-5 text-emerald-600" />
|
||||
<span>Security & CORS Headers</span>
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3 text-xs text-slate-600">
|
||||
<div className="flex items-center justify-between py-2 border-b border-slate-100">
|
||||
<span className="font-semibold text-slate-800">Access-Control-Allow-Origin</span>
|
||||
<code className="bg-slate-100 px-2 py-0.5 rounded text-indigo-600 font-mono">* (All Origins)</code>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2 border-b border-slate-100">
|
||||
<span className="font-semibold text-slate-800">Allowed HTTP Methods</span>
|
||||
<code className="bg-slate-100 px-2 py-0.5 rounded text-indigo-600 font-mono">GET, POST, PUT, DELETE</code>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2 border-b border-slate-100">
|
||||
<span className="font-semibold text-slate-800">MCP Protocol Header</span>
|
||||
<code className="bg-slate-100 px-2 py-0.5 rounded text-indigo-600 font-mono">X-MCP-Version: 2024-11-05</code>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="font-semibold text-slate-800">SSE Stream Timeout</span>
|
||||
<code className="bg-slate-100 px-2 py-0.5 rounded text-indigo-600 font-mono">15s Keep-Alive Ping</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OpenAPI Specification Section */}
|
||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm space-y-4">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-b border-slate-100 pb-4">
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-slate-900 flex items-center space-x-2">
|
||||
<FileCode className="h-5 w-5 text-indigo-600" />
|
||||
<span>OpenAPI 3.0 Specification</span>
|
||||
</h3>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Auto-generated OpenAPI schema for Swagger UI, Postman, or importing into external LLM agents.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={copySpec}
|
||||
className="px-3.5 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 font-medium text-xs transition-colors flex items-center space-x-1.5 border border-slate-200"
|
||||
>
|
||||
{copiedSpec ? <Check className="h-3.5 w-3.5 text-emerald-600" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
<span>{copiedSpec ? 'Copied JSON' : 'Copy Spec'}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={downloadOpenApiJSON}
|
||||
className="px-3.5 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-medium text-xs transition-all flex items-center space-x-1.5 shadow-sm shadow-indigo-200"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
<span>Export openapi.json</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Spec Json Preview */}
|
||||
<div className="p-4 bg-slate-900 text-indigo-300 rounded-xl text-xs font-mono overflow-x-auto max-h-80 border border-slate-800">
|
||||
{loadingSpec ? (
|
||||
<span className="text-slate-500">Loading OpenAPI schema...</span>
|
||||
) : (
|
||||
<pre>{JSON.stringify(openApiSpec, null, 2)}</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
368
src/components/ToolPlaygroundTab.tsx
Normal file
368
src/components/ToolPlaygroundTab.tsx
Normal file
@@ -0,0 +1,368 @@
|
||||
import React from 'react';
|
||||
import { Wrench, Play, Search, Copy, Check, Code, Clock, Terminal, Sparkles } from 'lucide-react';
|
||||
import { TOOLS } from '../server/tools/index';
|
||||
import { ToolDefinition, ToolExecutionResult, ToolParameterSchema } from '../types';
|
||||
|
||||
export const ToolPlaygroundTab: React.FC = () => {
|
||||
const [selectedCategory, setSelectedCategory] = React.useState<string>('all');
|
||||
const [searchQuery, setSearchQuery] = React.useState<string>('');
|
||||
const [selectedTool, setSelectedTool] = React.useState<ToolDefinition>(TOOLS[0]);
|
||||
|
||||
const [formValues, setFormValues] = React.useState<Record<string, any>>({});
|
||||
const [executionResult, setExecutionResult] = React.useState<ToolExecutionResult | null>(null);
|
||||
const [executing, setExecuting] = React.useState<boolean>(false);
|
||||
const [copiedCurl, setCopiedCurl] = React.useState<boolean>(false);
|
||||
|
||||
const categories = [
|
||||
{ id: 'all', label: 'All Tools' },
|
||||
{ id: 'web', label: 'Web & Scraping' },
|
||||
{ id: 'code', label: 'Code & Math' },
|
||||
{ id: 'data', label: 'Data & Crypto' },
|
||||
{ id: 'media', label: 'Charts & Media' },
|
||||
{ id: 'memory', label: 'Memory & AI' },
|
||||
{ id: 'network', label: 'Network & Utils' },
|
||||
];
|
||||
|
||||
// Initialize form default values when selectedTool changes
|
||||
React.useEffect(() => {
|
||||
const defaults: Record<string, any> = {};
|
||||
if (selectedTool && selectedTool.inputSchema && selectedTool.inputSchema.properties) {
|
||||
(Object.entries(selectedTool.inputSchema.properties) as [string, ToolParameterSchema][]).forEach(([key, prop]) => {
|
||||
if (prop.enum && prop.enum.length > 0) {
|
||||
defaults[key] = prop.enum[0];
|
||||
} else if (prop.type === 'number') {
|
||||
defaults[key] = 0;
|
||||
} else if (prop.type === 'array') {
|
||||
defaults[key] = [];
|
||||
} else if (prop.type === 'object') {
|
||||
defaults[key] = {};
|
||||
} else {
|
||||
defaults[key] = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Sample defaults for better instant testing demo
|
||||
if (selectedTool.name === 'web_scrape_markdown') {
|
||||
defaults['url'] = 'https://news.ycombinator.com';
|
||||
} else if (selectedTool.name === 'math_evaluator') {
|
||||
defaults['operation'] = 'eval';
|
||||
defaults['expression'] = 'Math.sqrt(256) * 12 + Math.pow(2, 8)';
|
||||
} else if (selectedTool.name === 'chart_generator') {
|
||||
defaults['title'] = 'Monthly Sales Performance';
|
||||
defaults['chartType'] = 'bar';
|
||||
defaults['labels'] = ['Jan', 'Feb', 'Mar', 'Apr', 'May'];
|
||||
defaults['values'] = [120, 190, 300, 250, 420];
|
||||
} else if (selectedTool.name === 'qr_barcode_generator') {
|
||||
defaults['text'] = 'https://github.com';
|
||||
defaults['size'] = 220;
|
||||
} else if (selectedTool.name === 'crypto_hash_generator') {
|
||||
defaults['algorithm'] = 'sha256';
|
||||
defaults['input'] = 'Do Everything API Secret Key';
|
||||
} else if (selectedTool.name === 'encoder_decoder') {
|
||||
defaults['action'] = 'base64_encode';
|
||||
defaults['input'] = 'Model Context Protocol 2024-11-05';
|
||||
} else if (selectedTool.name === 'execute_js') {
|
||||
defaults['code'] = 'const numbers = [10, 20, 30, 40];\nconsole.log("Sum:", numbers.reduce((a,b) => a+b, 0));\nreturn "Calculation finished successfully";';
|
||||
} else if (selectedTool.name === 'memory_store_set') {
|
||||
defaults['key'] = 'agent_preference_theme';
|
||||
defaults['value'] = { mode: 'dark', fontSize: '14px', autoSave: true };
|
||||
defaults['tags'] = ['preferences', 'ui'];
|
||||
}
|
||||
}
|
||||
setFormValues(defaults);
|
||||
setExecutionResult(null);
|
||||
}, [selectedTool]);
|
||||
|
||||
const filteredTools = TOOLS.filter(t => {
|
||||
const matchesCat = selectedCategory === 'all' || t.category === selectedCategory;
|
||||
const matchesQuery =
|
||||
t.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
t.description.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
return matchesCat && matchesQuery;
|
||||
});
|
||||
|
||||
const handleInputChange = (key: string, value: any, type?: string) => {
|
||||
if (type === 'number') {
|
||||
setFormValues(prev => ({ ...prev, [key]: Number(value) }));
|
||||
} else if (type === 'array' && typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
setFormValues(prev => ({ ...prev, [key]: parsed }));
|
||||
} catch {
|
||||
// Fallback comma split
|
||||
setFormValues(prev => ({ ...prev, [key]: value.split(',').map(s => s.trim()) }));
|
||||
}
|
||||
} else {
|
||||
setFormValues(prev => ({ ...prev, [key]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleExecute = async () => {
|
||||
setExecuting(true);
|
||||
setExecutionResult(null);
|
||||
try {
|
||||
const res = await fetch(`/api/v1/tools/${selectedTool.name}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formValues),
|
||||
});
|
||||
const data: ToolExecutionResult = await res.json();
|
||||
setExecutionResult(data);
|
||||
} catch (err: any) {
|
||||
setExecutionResult({
|
||||
success: false,
|
||||
toolName: selectedTool.name,
|
||||
result: null,
|
||||
error: err.message || 'Execution failed',
|
||||
executionTimeMs: 0,
|
||||
});
|
||||
} finally {
|
||||
setExecuting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateCurl = () => {
|
||||
const baseUrl = window.location.origin;
|
||||
return `curl -X POST "${baseUrl}/api/v1/tools/${selectedTool.name}" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '${JSON.stringify(formValues, null, 2)}'`;
|
||||
};
|
||||
|
||||
const copyCurl = () => {
|
||||
navigator.clipboard.writeText(generateCurl());
|
||||
setCopiedCurl(true);
|
||||
setTimeout(() => setCopiedCurl(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Search & Category Filter Bar */}
|
||||
<div className="bg-white rounded-2xl p-4 border border-slate-200 shadow-sm space-y-3">
|
||||
<div className="flex flex-col sm:flex-row items-center gap-3">
|
||||
<div className="relative w-full sm:w-72">
|
||||
<Search className="absolute left-3 top-2.5 h-4 w-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search tools..."
|
||||
className="w-full pl-9 pr-3 py-2 border border-slate-200 rounded-xl text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5 w-full">
|
||||
{categories.map(cat => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setSelectedCategory(cat.id)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${
|
||||
selectedCategory === cat.id
|
||||
? 'bg-indigo-600 text-white shadow-xs'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{cat.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Grid: Tool List vs Execution Sandbox */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
{/* Tool List Sidebar */}
|
||||
<div className="lg:col-span-4 bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden max-h-[680px] flex flex-col">
|
||||
<div className="p-3 bg-slate-50 border-b border-slate-200 flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-slate-700 uppercase tracking-wider">
|
||||
Available Tools ({filteredTools.length})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-100 overflow-y-auto">
|
||||
{filteredTools.map(t => {
|
||||
const isSelected = selectedTool.name === t.name;
|
||||
return (
|
||||
<button
|
||||
key={t.name}
|
||||
onClick={() => setSelectedTool(t)}
|
||||
className={`w-full text-left p-3.5 transition-all flex items-start space-x-3 ${
|
||||
isSelected ? 'bg-indigo-50/70 border-l-4 border-indigo-600' : 'hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<div className="p-2 rounded-lg bg-slate-100 text-slate-700 mt-0.5">
|
||||
<Wrench className="h-4 w-4 text-indigo-600" />
|
||||
</div>
|
||||
<div className="space-y-0.5 min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold font-mono text-slate-900 truncate">{t.name}</span>
|
||||
<span className="text-[10px] uppercase font-semibold px-1.5 py-0.5 rounded bg-slate-100 text-slate-500">
|
||||
{t.category}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 line-clamp-2 leading-snug">{t.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Execution Sandbox Form & Output */}
|
||||
<div className="lg:col-span-8 space-y-6">
|
||||
{/* Tool Definition Header */}
|
||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="text-lg font-bold font-mono text-slate-900">{selectedTool.name}</h3>
|
||||
<span className="px-2.5 py-0.5 text-xs font-semibold rounded-full bg-indigo-100 text-indigo-800 uppercase">
|
||||
{selectedTool.category}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-600 mt-1">{selectedTool.description}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={copyCurl}
|
||||
className="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-slate-100 hover:bg-slate-200 text-slate-700 transition-colors border border-slate-200"
|
||||
>
|
||||
{copiedCurl ? <Check className="h-3.5 w-3.5 text-emerald-600" /> : <Code className="h-3.5 w-3.5" />}
|
||||
<span>{copiedCurl ? 'Copied cURL' : 'Copy cURL'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Dynamic Form Parameters */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-700">Input Arguments</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{selectedTool.inputSchema &&
|
||||
selectedTool.inputSchema.properties &&
|
||||
(Object.entries(selectedTool.inputSchema.properties) as [string, ToolParameterSchema][]).map(([key, prop]) => {
|
||||
const isRequired = selectedTool.inputSchema.required?.includes(key);
|
||||
return (
|
||||
<div key={key} className={prop.type === 'object' || key === 'code' || key === 'text' || key === 'originalText' ? 'sm:col-span-2' : ''}>
|
||||
<label className="block text-xs font-semibold text-slate-800 mb-1">
|
||||
<span className="font-mono text-indigo-700">{key}</span>
|
||||
{isRequired && <span className="text-rose-500 ml-1">*</span>}
|
||||
<span className="text-slate-400 text-[11px] font-normal ml-2">({prop.type})</span>
|
||||
</label>
|
||||
|
||||
{prop.description && (
|
||||
<p className="text-[11px] text-slate-500 mb-1.5">{prop.description}</p>
|
||||
)}
|
||||
|
||||
{prop.enum && prop.enum.length > 0 ? (
|
||||
<select
|
||||
value={formValues[key] || prop.enum[0]}
|
||||
onChange={(e) => handleInputChange(key, e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white"
|
||||
>
|
||||
{prop.enum.map(opt => (
|
||||
<option key={opt} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : key === 'code' || key === 'originalText' || key === 'modifiedText' ? (
|
||||
<textarea
|
||||
rows={4}
|
||||
value={formValues[key] || ''}
|
||||
onChange={(e) => handleInputChange(key, e.target.value)}
|
||||
className="w-full p-3 border border-slate-300 rounded-xl text-xs font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-slate-50"
|
||||
/>
|
||||
) : prop.type === 'array' || prop.type === 'object' ? (
|
||||
<input
|
||||
type="text"
|
||||
value={
|
||||
typeof formValues[key] === 'object'
|
||||
? JSON.stringify(formValues[key])
|
||||
: formValues[key] || ''
|
||||
}
|
||||
onChange={(e) => handleInputChange(key, e.target.value, prop.type)}
|
||||
placeholder={prop.type === 'array' ? 'JSON array or comma separated' : 'JSON string'}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl text-xs font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type={prop.type === 'number' ? 'number' : 'text'}
|
||||
value={formValues[key] !== undefined ? formValues[key] : ''}
|
||||
onChange={(e) => handleInputChange(key, e.target.value, prop.type)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-xl text-xs font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleExecute}
|
||||
disabled={executing}
|
||||
className="w-full py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-medium text-sm transition-all shadow-md shadow-indigo-200 flex items-center justify-center space-x-2 disabled:opacity-50"
|
||||
>
|
||||
{executing ? (
|
||||
<span>Executing Tool...</span>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4 fill-current" />
|
||||
<span>Run Tool Request</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Execution Result Display */}
|
||||
{executionResult && (
|
||||
<div className="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span
|
||||
className={`h-2.5 w-2.5 rounded-full ${
|
||||
executionResult.success ? 'bg-emerald-500' : 'bg-rose-500'
|
||||
}`}
|
||||
></span>
|
||||
<h4 className="text-sm font-bold text-slate-900">
|
||||
Execution Result: {executionResult.success ? 'SUCCESS' : 'ERROR'}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 text-xs font-mono text-slate-500">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
<span>{executionResult.executionTimeMs} ms</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{executionResult.error ? (
|
||||
<div className="p-4 bg-rose-50 border border-rose-200 rounded-xl text-xs text-rose-700 font-mono">
|
||||
{executionResult.error}
|
||||
</div>
|
||||
) : executionResult.contentType === 'svg' ? (
|
||||
<div className="p-4 bg-slate-50 border border-slate-200 rounded-xl flex flex-col items-center justify-center space-y-4">
|
||||
<div
|
||||
className="max-w-md w-full overflow-hidden shadow-sm rounded-lg"
|
||||
dangerouslySetInnerHTML={{ __html: executionResult.result }}
|
||||
/>
|
||||
<details className="w-full text-xs">
|
||||
<summary className="cursor-pointer text-indigo-600 font-semibold mb-2">View Raw SVG Code</summary>
|
||||
<pre className="p-3 bg-slate-900 text-indigo-300 rounded-lg text-[11px] font-mono overflow-x-auto">
|
||||
{executionResult.result}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
) : executionResult.contentType === 'markdown' ? (
|
||||
<div className="p-4 bg-slate-900 text-slate-100 rounded-xl text-xs font-mono overflow-x-auto whitespace-pre-wrap leading-relaxed">
|
||||
{executionResult.result}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 bg-slate-900 text-emerald-400 rounded-xl text-xs font-mono overflow-x-auto max-h-96">
|
||||
<pre>{JSON.stringify(executionResult.result, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
206
src/components/WorkflowsTab.tsx
Normal file
206
src/components/WorkflowsTab.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { GitBranch, Play, Plus, Trash2, ArrowRight, CheckCircle2, AlertCircle, RefreshCw, Layers, Code, Zap } from 'lucide-react';
|
||||
|
||||
interface WorkflowStep {
|
||||
stepId: string;
|
||||
name: string;
|
||||
toolName: string;
|
||||
argsTemplate: Record<string, any>;
|
||||
}
|
||||
|
||||
interface MCPWorkflow {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
steps: WorkflowStep[];
|
||||
createdAt: string;
|
||||
lastExecution?: {
|
||||
executedAt: string;
|
||||
durationMs: number;
|
||||
success: boolean;
|
||||
stepResults: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
export const WorkflowsTab: React.FC = () => {
|
||||
const [workflows, setWorkflows] = useState<MCPWorkflow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [executingId, setExecutingId] = useState<string | null>(null);
|
||||
const [inputsJson, setInputsJson] = useState<Record<string, string>>({
|
||||
'wf-preset-summarize-slack': '{"url": "https://en.wikipedia.org/wiki/Model_Context_Protocol"}',
|
||||
});
|
||||
const [executionResult, setExecutionResult] = useState<any | null>(null);
|
||||
|
||||
const fetchWorkflows = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/v1/mcp/workflows');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setWorkflows(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchWorkflows();
|
||||
}, []);
|
||||
|
||||
const handleExecute = async (wfId: string) => {
|
||||
setExecutingId(wfId);
|
||||
setExecutionResult(null);
|
||||
|
||||
let inputObj = {};
|
||||
try {
|
||||
const rawInput = inputsJson[wfId] || '{}';
|
||||
inputObj = JSON.parse(rawInput);
|
||||
} catch (e) {
|
||||
alert('Invalid JSON in pipeline input arguments');
|
||||
setExecutingId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/v1/mcp/workflows/${wfId}/execute`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(inputObj),
|
||||
});
|
||||
const data = await res.json();
|
||||
setExecutionResult({ wfId, data });
|
||||
fetchWorkflows();
|
||||
} catch (e: any) {
|
||||
setExecutionResult({ wfId, data: { success: false, error: e.message } });
|
||||
} finally {
|
||||
setExecutingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header Banner */}
|
||||
<div className="bg-gradient-to-r from-slate-900 via-indigo-950 to-slate-900 rounded-2xl p-6 sm:p-8 text-white shadow-xl relative overflow-hidden">
|
||||
<div className="relative z-10 max-w-3xl">
|
||||
<span className="inline-flex items-center space-x-1.5 px-3 py-1 rounded-full text-xs font-semibold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30 mb-3">
|
||||
<GitBranch className="h-3.5 w-3.5" />
|
||||
<span>MCP Tool Sequence Engine</span>
|
||||
</span>
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold tracking-tight">Multi-Tool Workflows & Pipelines</h2>
|
||||
<p className="mt-2 text-slate-300 text-sm leading-relaxed">
|
||||
Compose multiple MCP tools into automated sequential pipelines. Output variables from prior steps (e.g. <code className="text-amber-300 font-mono text-xs">{`{{step1.result.markdown}}`}</code>) pass seamlessly into downstream tool parameters.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflows List */}
|
||||
<div className="space-y-6">
|
||||
{loading ? (
|
||||
<div className="bg-white rounded-2xl p-12 text-center text-slate-500 border border-slate-200">
|
||||
<RefreshCw className="h-6 w-6 animate-spin mx-auto mb-2 text-indigo-600" />
|
||||
<span>Loading MCP Pipelines...</span>
|
||||
</div>
|
||||
) : workflows.length === 0 ? (
|
||||
<div className="bg-white rounded-2xl p-12 text-center text-slate-500 border border-slate-200">
|
||||
<GitBranch className="h-8 w-8 mx-auto mb-2 text-slate-400" />
|
||||
<p className="font-semibold text-slate-700">No active pipelines created yet</p>
|
||||
</div>
|
||||
) : (
|
||||
workflows.map((wf) => (
|
||||
<div key={wf.id} className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-4 border-b border-slate-200">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="px-2.5 py-0.5 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-800 font-mono">
|
||||
{wf.steps.length} Steps
|
||||
</span>
|
||||
<h3 className="text-lg font-bold text-slate-900">{wf.name}</h3>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">{wf.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={() => handleExecute(wf.id)}
|
||||
disabled={executingId === wf.id}
|
||||
className="inline-flex items-center space-x-2 px-4 py-2 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-medium text-xs transition-all shadow-md shadow-emerald-600/20 disabled:opacity-50"
|
||||
>
|
||||
{executingId === wf.id ? (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
<span>Executing Pipeline...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4 fill-current" />
|
||||
<span>Run Pipeline</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visual Pipeline Flow */}
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-slate-500 mb-3 uppercase tracking-wider">Pipeline Flow Diagram</h4>
|
||||
<div className="flex flex-col md:flex-row items-center gap-3 overflow-x-auto pb-2">
|
||||
<div className="bg-slate-100 rounded-xl p-3 border border-slate-200 text-xs w-full md:w-48 shrink-0">
|
||||
<span className="font-semibold text-slate-700 block">0. Initial Input</span>
|
||||
<span className="text-[10px] text-slate-500 font-mono block mt-1">{`{{input.url}}`}</span>
|
||||
</div>
|
||||
|
||||
{wf.steps.map((step, idx) => (
|
||||
<React.Fragment key={step.stepId}>
|
||||
<ArrowRight className="h-4 w-4 text-slate-400 shrink-0 hidden md:block" />
|
||||
<div className="bg-indigo-50/70 rounded-xl p-3 border border-indigo-200 text-xs w-full md:w-60 shrink-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="font-bold text-indigo-900">Step {idx + 1}: {step.name}</span>
|
||||
<span className="text-[10px] font-mono bg-indigo-200/60 text-indigo-800 px-1.5 py-0.5 rounded">
|
||||
{step.toolName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-600 font-mono overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{JSON.stringify(step.argsTemplate)}
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input Args & Runner */}
|
||||
<div className="bg-slate-50 rounded-xl p-4 border border-slate-200 space-y-3">
|
||||
<label className="block text-xs font-semibold text-slate-700">Pipeline Input JSON Arguments:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inputsJson[wf.id] || '{"url": "https://en.wikipedia.org/wiki/Model_Context_Protocol"}'}
|
||||
onChange={(e) => setInputsJson({ ...inputsJson, [wf.id]: e.target.value })}
|
||||
className="w-full rounded-lg border border-slate-300 p-2.5 font-mono text-xs bg-white text-slate-900 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Execution Result */}
|
||||
{executionResult && executionResult.wfId === wf.id && (
|
||||
<div className="bg-slate-900 rounded-xl p-4 text-slate-100 font-mono text-xs space-y-2">
|
||||
<div className="flex items-center justify-between pb-2 border-b border-slate-800">
|
||||
<span className="text-indigo-400 font-bold">Execution Output (Duration: {executionResult.data.durationMs}ms):</span>
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] ${executionResult.data.success ? 'bg-emerald-900 text-emerald-300' : 'bg-red-900 text-red-300'}`}>
|
||||
{executionResult.data.success ? 'SUCCESS' : 'FAILED'}
|
||||
</span>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap text-emerald-300 max-h-60 overflow-y-auto">
|
||||
{JSON.stringify(executionResult.data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
src/index.css
Normal file
1
src/index.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
165
src/server/aiGenerator.ts
Normal file
165
src/server/aiGenerator.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { GoogleGenAI } from '@google/genai';
|
||||
import { MCPServerDefinition, ToolDefinition } from '../types.js';
|
||||
|
||||
let aiClient: GoogleGenAI | null = null;
|
||||
|
||||
function getAIClient(): GoogleGenAI | null {
|
||||
if (!aiClient) {
|
||||
const key = process.env.GEMINI_API_KEY;
|
||||
if (key && key !== 'MY_GEMINI_API_KEY') {
|
||||
aiClient = new GoogleGenAI({ apiKey: key });
|
||||
}
|
||||
}
|
||||
return aiClient;
|
||||
}
|
||||
|
||||
export interface GeneratedMCPResponse {
|
||||
serverName: string;
|
||||
description: string;
|
||||
category: MCPServerDefinition['category'];
|
||||
type: 'custom_script' | 'webhook';
|
||||
toolName: string;
|
||||
toolDescription: string;
|
||||
inputSchema: {
|
||||
type: 'object';
|
||||
properties: Record<string, any>;
|
||||
required?: string[];
|
||||
};
|
||||
customScript?: string;
|
||||
webhookConfig?: {
|
||||
url: string;
|
||||
method: 'GET' | 'POST' | 'PUT';
|
||||
headers?: Record<string, string>;
|
||||
bodyTemplate?: string;
|
||||
};
|
||||
sampleArgs: Record<string, any>;
|
||||
}
|
||||
|
||||
export async function generateMCPToolFromPrompt(prompt: string): Promise<GeneratedMCPResponse> {
|
||||
const ai = getAIClient();
|
||||
|
||||
if (!ai) {
|
||||
// Fallback template generator when GEMINI_API_KEY is not configured
|
||||
return generateFallbackMCP(prompt);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash',
|
||||
contents: `You are an expert Model Context Protocol (MCP) tool builder.
|
||||
Given the following user request or API prompt, construct a complete MCP Tool configuration in strict JSON.
|
||||
|
||||
User Request: "${prompt}"
|
||||
|
||||
Respond ONLY with valid JSON (no markdown ticks or commentary) matching this TypeScript schema:
|
||||
{
|
||||
"serverName": "descriptive server name (e.g. Stock Ticker MCP)",
|
||||
"description": "clear description of what this server does",
|
||||
"category": "web" | "code" | "data" | "media" | "memory" | "network" | "custom" | "dev",
|
||||
"type": "custom_script" or "webhook",
|
||||
"toolName": "snake_case_tool_name",
|
||||
"toolDescription": "detailed description of what the tool accomplishes",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"param1": { "type": "string", "description": "param description" }
|
||||
},
|
||||
"required": ["param1"]
|
||||
},
|
||||
"customScript": "Async JavaScript function body code using (args, fetch). Return a JSON object. Example: const res = await fetch(\`https://api.example.com/data?q=\${encodeURIComponent(args.query)}\`); return await res.json();",
|
||||
"webhookConfig": {
|
||||
"url": "https://api.example.com/v1/resource?id={{id}}",
|
||||
"method": "GET" or "POST",
|
||||
"bodyTemplate": "{\\"query\\": \\"{{query}}\\"}"
|
||||
},
|
||||
"sampleArgs": { "param1": "sample_value" }
|
||||
}
|
||||
|
||||
Provide executable JavaScript code for customScript or accurate URL template for webhookConfig. Use fetch() if HTTP calls are needed inside customScript.`,
|
||||
});
|
||||
|
||||
const rawText = response.text || '';
|
||||
const cleanJson = rawText.replace(/```json/g, '').replace(/```/g, '').trim();
|
||||
const parsed = JSON.parse(cleanJson);
|
||||
return parsed;
|
||||
} catch (err) {
|
||||
console.warn('Gemini API call failed, using heuristic fallback:', err);
|
||||
return generateFallbackMCP(prompt);
|
||||
}
|
||||
}
|
||||
|
||||
function generateFallbackMCP(prompt: string): GeneratedMCPResponse {
|
||||
const cleanPrompt = prompt.trim();
|
||||
const lower = cleanPrompt.toLowerCase();
|
||||
|
||||
let toolName = cleanPrompt.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/(^_|_$)/g, '');
|
||||
if (!toolName || toolName.length < 3) toolName = 'ai_custom_mcp_tool';
|
||||
|
||||
if (lower.includes('calc') || fontCheck(lower, ['math', 'sum', 'convert', 'tax', 'price'])) {
|
||||
return {
|
||||
serverName: `${capitalize(cleanPrompt)} MCP Tool`,
|
||||
description: `Calculates and evaluates data based on: ${cleanPrompt}`,
|
||||
category: 'code',
|
||||
type: 'custom_script',
|
||||
toolName: `${toolName}_calculator`,
|
||||
toolDescription: `Perform evaluation and conversion for ${cleanPrompt}`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
inputVal: { type: 'string', description: 'Input value or mathematical expression' },
|
||||
unit: { type: 'string', description: 'Target unit or operation mode' },
|
||||
},
|
||||
required: ['inputVal'],
|
||||
},
|
||||
customScript: `
|
||||
// Custom generated JS evaluator
|
||||
const val = args.inputVal || '0';
|
||||
const num = parseFloat(val) || 0;
|
||||
return {
|
||||
originalInput: val,
|
||||
numericValue: num,
|
||||
doubled: num * 2,
|
||||
squared: Math.pow(num, 2),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
`,
|
||||
sampleArgs: { inputVal: '42', unit: 'metric' },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
serverName: `${capitalize(cleanPrompt)} MCP`,
|
||||
description: `Auto-generated MCP tool for processing: ${cleanPrompt}`,
|
||||
category: 'custom',
|
||||
type: 'custom_script',
|
||||
toolName: toolName,
|
||||
toolDescription: `Processes and transforms data according to request: ${cleanPrompt}`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string', description: 'Text or data payload to analyze or transform' },
|
||||
},
|
||||
required: ['text'],
|
||||
},
|
||||
customScript: `
|
||||
// Generated JavaScript handler
|
||||
const input = args.text || '';
|
||||
return {
|
||||
status: 'success',
|
||||
processedText: input.toUpperCase(),
|
||||
charCount: input.length,
|
||||
wordCount: input.trim().split(/\\s+/).filter(Boolean).length,
|
||||
processedAt: new Date().toISOString()
|
||||
};
|
||||
`,
|
||||
sampleArgs: { text: 'Hello MCP Hub World!' },
|
||||
};
|
||||
}
|
||||
|
||||
function fontCheck(str: string, words: string[]) {
|
||||
return words.some(w => str.includes(w));
|
||||
}
|
||||
|
||||
function capitalize(s: string) {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
258
src/server/mcp.ts
Normal file
258
src/server/mcp.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import { mcpRegistry } from './mcpRegistry.js';
|
||||
import { memoryStore } from './memory.js';
|
||||
import { MCPRequest, MCPResponse } from '../types.js';
|
||||
|
||||
export async function handleMCPMessage(request: MCPRequest): Promise<MCPResponse> {
|
||||
const { id, method, params } = request;
|
||||
|
||||
switch (method) {
|
||||
case 'initialize': {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
prompts: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: 'do-everything-mcp-hub-server',
|
||||
version: '1.0.0',
|
||||
description: 'Self-hosted Model Context Protocol (MCP) Hub & Server Registry',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'notifications/initialized': {
|
||||
return { jsonrpc: '2.0', id, result: {} };
|
||||
}
|
||||
|
||||
case 'ping': {
|
||||
return { jsonrpc: '2.0', id, result: {} };
|
||||
}
|
||||
|
||||
case 'tools/list': {
|
||||
const activeTools = mcpRegistry.getAllActiveTools();
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
tools: activeTools.map(t => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
inputSchema: t.inputSchema,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'tools/call': {
|
||||
const { name, arguments: args = {} } = params || {};
|
||||
if (!name) {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
error: { code: -32602, message: 'Invalid params: tool "name" is required' },
|
||||
};
|
||||
}
|
||||
|
||||
const execResult = await mcpRegistry.executeServerTool(name, args);
|
||||
|
||||
if (!execResult.success) {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
content: [{ type: 'text', text: `MCP Tool Error: ${execResult.error}` }],
|
||||
isError: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const formattedOutput =
|
||||
typeof execResult.result === 'string'
|
||||
? execResult.result
|
||||
: JSON.stringify(execResult.result, null, 2);
|
||||
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
content: [
|
||||
{
|
||||
type: execResult.contentType === 'svg' ? 'text' : 'text',
|
||||
text: formattedOutput,
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'resources/list': {
|
||||
const activeTools = mcpRegistry.getAllActiveTools();
|
||||
const servers = mcpRegistry.getServers();
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
resources: [
|
||||
{
|
||||
uri: 'mcp://hub/registry',
|
||||
name: 'MCP Hub Server Registry',
|
||||
description: `List of ${servers.length} registered MCP servers and custom tools`,
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: 'memory://store',
|
||||
name: 'Agent Memory Vault',
|
||||
description: 'Long-term key-value and tag memory store for connected AI agents',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: 'system://status',
|
||||
name: 'Server Health & Metrics',
|
||||
description: 'Real-time metrics, uptime, registered tools count, and memory status',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: 'tools://catalog',
|
||||
name: 'Tool Suite Catalog',
|
||||
description: `Complete specification of all ${activeTools.length} active MCP tools`,
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'resources/read': {
|
||||
const { uri } = params || {};
|
||||
let contentText = '';
|
||||
|
||||
if (uri === 'mcp://hub/registry') {
|
||||
contentText = JSON.stringify(mcpRegistry.getServers(), null, 2);
|
||||
} else if (uri === 'memory://store') {
|
||||
contentText = JSON.stringify(memoryStore.list(), null, 2);
|
||||
} else if (uri === 'system://status') {
|
||||
const activeTools = mcpRegistry.getAllActiveTools();
|
||||
const servers = mcpRegistry.getServers();
|
||||
contentText = JSON.stringify(
|
||||
{
|
||||
status: 'online',
|
||||
uptimeSeconds: process.uptime(),
|
||||
memoryUsageMb: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
||||
activeMemoryItems: memoryStore.count(),
|
||||
registeredTools: activeTools.length,
|
||||
registeredServers: servers.length,
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
} else if (uri === 'tools://catalog') {
|
||||
contentText = JSON.stringify(mcpRegistry.getAllActiveTools(), null, 2);
|
||||
} else {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
error: { code: -32602, message: `Resource URI not found: ${uri}` },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: contentText,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'prompts/list': {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
prompts: [
|
||||
{
|
||||
name: 'web-research',
|
||||
description: 'Scrapes, searches, and synthesizes web content into a structured research memo.',
|
||||
arguments: [{ name: 'topic', description: 'Target research topic or URL', required: true }],
|
||||
},
|
||||
{
|
||||
name: 'data-transform',
|
||||
description: 'Converts unstructured input data into CSV/JSON with hashes and clean metadata.',
|
||||
arguments: [{ name: 'data', description: 'Raw input data string', required: true }],
|
||||
},
|
||||
{
|
||||
name: 'visual-chart',
|
||||
description: 'Generates an SVG chart visualization for data points.',
|
||||
arguments: [{ name: 'title', description: 'Chart header title', required: true }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'prompts/get': {
|
||||
const { name, arguments: promptArgs = {} } = params || {};
|
||||
if (name === 'web-research') {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
description: 'Web Research Prompt',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Please conduct deep research on "${promptArgs.topic}". Use the web_scrape_markdown and web_search_gemini tools to gather sources, extract key points, and store findings in memory_store_set.`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
} else if (name === 'data-transform') {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result: {
|
||||
description: 'Data Transform Prompt',
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Transform this data into clean structured JSON and generate a SHA-256 fingerprint hash: "${promptArgs.data}". Use data_converter and crypto_hash_generator.`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
error: { code: -32602, message: `Prompt "${name}" not found.` },
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
error: { code: -32601, message: `Method not found: ${method}` },
|
||||
};
|
||||
}
|
||||
}
|
||||
2322
src/server/mcpRegistry.ts
Normal file
2322
src/server/mcpRegistry.ts
Normal file
File diff suppressed because it is too large
Load Diff
94
src/server/mcpSecurity.ts
Normal file
94
src/server/mcpSecurity.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
export interface AgentKey {
|
||||
id: string;
|
||||
name: string;
|
||||
key: string;
|
||||
scope: 'full' | 'read_only' | 'restricted';
|
||||
allowedTools?: string[];
|
||||
rateLimitPerMin: number;
|
||||
requestCount: number;
|
||||
lastUsedAt?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
class MCPSecurityManager {
|
||||
private keys: Map<string, AgentKey> = new Map();
|
||||
private authRequired: boolean = false;
|
||||
|
||||
constructor() {
|
||||
// Default demo key
|
||||
const defaultKey: AgentKey = {
|
||||
id: 'key-default-demo',
|
||||
name: 'Claude Desktop Agent Key',
|
||||
key: 'mcp_live_sec_claude_desktop_98123',
|
||||
scope: 'full',
|
||||
rateLimitPerMin: 120,
|
||||
requestCount: 14,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
this.keys.set(defaultKey.key, defaultKey);
|
||||
}
|
||||
|
||||
public isAuthRequired(): boolean {
|
||||
return this.authRequired;
|
||||
}
|
||||
|
||||
public setAuthRequired(required: boolean) {
|
||||
this.authRequired = required;
|
||||
}
|
||||
|
||||
public getKeys(): AgentKey[] {
|
||||
return Array.from(this.keys.values());
|
||||
}
|
||||
|
||||
public createKey(name: string, scope: AgentKey['scope'] = 'full', rateLimitPerMin: number = 60, allowedTools?: string[]): AgentKey {
|
||||
const randomHex = Math.random().toString(36).substring(2, 12) + Math.random().toString(36).substring(2, 12);
|
||||
const key = `mcp_live_sec_${randomHex}`;
|
||||
const id = `key-${Date.now()}`;
|
||||
const agentKey: AgentKey = {
|
||||
id,
|
||||
name,
|
||||
key,
|
||||
scope,
|
||||
rateLimitPerMin,
|
||||
requestCount: 0,
|
||||
allowedTools,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
this.keys.set(key, agentKey);
|
||||
return agentKey;
|
||||
}
|
||||
|
||||
public revokeKey(id: string): boolean {
|
||||
for (const [k, v] of this.keys.entries()) {
|
||||
if (v.id === id) {
|
||||
this.keys.delete(k);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public validateKey(authHeader?: string): { valid: boolean; keyObject?: AgentKey; error?: string } {
|
||||
if (!this.authRequired) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
if (!authHeader) {
|
||||
return { valid: false, error: 'Missing Authorization header' };
|
||||
}
|
||||
|
||||
const token = authHeader.replace(/^Bearer\s+/i, '').trim();
|
||||
const keyObj = this.keys.get(token);
|
||||
|
||||
if (!keyObj) {
|
||||
return { valid: false, error: 'Invalid or revoked MCP Agent Key' };
|
||||
}
|
||||
|
||||
keyObj.requestCount += 1;
|
||||
keyObj.lastUsedAt = new Date().toISOString();
|
||||
return { valid: true, keyObject: keyObj };
|
||||
}
|
||||
}
|
||||
|
||||
export const mcpSecurityManager = new MCPSecurityManager();
|
||||
213
src/server/mcpWorkflows.ts
Normal file
213
src/server/mcpWorkflows.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { mcpRegistry } from './mcpRegistry.js';
|
||||
import { ToolExecutionResult } from '../types.js';
|
||||
|
||||
export interface WorkflowStep {
|
||||
stepId: string;
|
||||
name: string;
|
||||
toolName: string;
|
||||
// Arguments mapping template: e.g. { "url": "{{input.url}}", "text": "{{step1.result.markdown}}" }
|
||||
argsTemplate: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface MCPWorkflow {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
steps: WorkflowStep[];
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
lastExecution?: {
|
||||
executedAt: string;
|
||||
durationMs: number;
|
||||
success: boolean;
|
||||
stepResults: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
class MCPWorkflowManager {
|
||||
private workflows: Map<string, MCPWorkflow> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.initializePresets();
|
||||
}
|
||||
|
||||
private initializePresets() {
|
||||
const sampleWorkflow: MCPWorkflow = {
|
||||
id: 'wf-preset-summarize-slack',
|
||||
name: 'Web Scrape & Sentiment Pipeline',
|
||||
description: 'Scrapes HTML from target URL, converts to markdown, then extracts top keywords and text sentiment.',
|
||||
enabled: true,
|
||||
steps: [
|
||||
{
|
||||
stepId: 'step1',
|
||||
name: 'Fetch Web Content',
|
||||
toolName: 'web_scrape_markdown',
|
||||
argsTemplate: {
|
||||
url: '{{input.url}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
stepId: 'step2',
|
||||
name: 'Analyze Sentiment & Keywords',
|
||||
toolName: 'extract_text_keywords_mcp',
|
||||
argsTemplate: {
|
||||
text: '{{step1.result.markdown}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
this.workflows.set(sampleWorkflow.id, sampleWorkflow);
|
||||
}
|
||||
|
||||
public getWorkflows(): MCPWorkflow[] {
|
||||
return Array.from(this.workflows.values());
|
||||
}
|
||||
|
||||
public getWorkflow(id: string): MCPWorkflow | undefined {
|
||||
return this.workflows.get(id);
|
||||
}
|
||||
|
||||
public createWorkflow(wf: Omit<MCPWorkflow, 'id' | 'createdAt'>): MCPWorkflow {
|
||||
const id = `wf-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
|
||||
const newWf: MCPWorkflow = {
|
||||
...wf,
|
||||
id,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
this.workflows.set(id, newWf);
|
||||
return newWf;
|
||||
}
|
||||
|
||||
public updateWorkflow(id: string, updates: Partial<MCPWorkflow>): MCPWorkflow | null {
|
||||
const existing = this.workflows.get(id);
|
||||
if (!existing) return null;
|
||||
const updated = {
|
||||
...existing,
|
||||
...updates,
|
||||
id,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
this.workflows.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
public deleteWorkflow(id: string): boolean {
|
||||
return this.workflows.delete(id);
|
||||
}
|
||||
|
||||
public async executeWorkflow(id: string, initialInputs: Record<string, any>): Promise<{
|
||||
success: boolean;
|
||||
durationMs: number;
|
||||
stepResults: Record<string, ToolExecutionResult>;
|
||||
finalOutput: any;
|
||||
error?: string;
|
||||
}> {
|
||||
const startTime = Date.now();
|
||||
const wf = this.workflows.get(id);
|
||||
if (!wf) {
|
||||
return {
|
||||
success: false,
|
||||
durationMs: 0,
|
||||
stepResults: {},
|
||||
finalOutput: null,
|
||||
error: `Workflow "${id}" not found`,
|
||||
};
|
||||
}
|
||||
|
||||
const stepResults: Record<string, ToolExecutionResult> = {};
|
||||
const contextData: Record<string, any> = { input: initialInputs };
|
||||
|
||||
for (const step of wf.steps) {
|
||||
// Interpolate arguments template from contextData
|
||||
const resolvedArgs = this.resolveArgsTemplate(step.argsTemplate, contextData);
|
||||
|
||||
// Execute tool
|
||||
const execRes = await mcpRegistry.executeServerTool(step.toolName, resolvedArgs);
|
||||
stepResults[step.stepId] = execRes;
|
||||
contextData[step.stepId] = execRes;
|
||||
|
||||
if (!execRes.success) {
|
||||
const durationMs = Date.now() - startTime;
|
||||
wf.lastExecution = {
|
||||
executedAt: new Date().toISOString(),
|
||||
durationMs,
|
||||
success: false,
|
||||
stepResults,
|
||||
};
|
||||
return {
|
||||
success: false,
|
||||
durationMs,
|
||||
stepResults,
|
||||
finalOutput: null,
|
||||
error: `Pipeline failed at step "${step.name}" (${step.toolName}): ${execRes.error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const durationMs = Date.now() - startTime;
|
||||
const lastStepId = wf.steps[wf.steps.length - 1]?.stepId;
|
||||
const finalOutput = lastStepId ? stepResults[lastStepId]?.result : contextData;
|
||||
|
||||
wf.lastExecution = {
|
||||
executedAt: new Date().toISOString(),
|
||||
durationMs,
|
||||
success: true,
|
||||
stepResults,
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
durationMs,
|
||||
stepResults,
|
||||
finalOutput,
|
||||
};
|
||||
}
|
||||
|
||||
private resolveArgsTemplate(template: Record<string, any>, context: Record<string, any>): Record<string, any> {
|
||||
const resolved: Record<string, any> = {};
|
||||
|
||||
for (const [key, val] of Object.entries(template)) {
|
||||
if (typeof val === 'string') {
|
||||
resolved[key] = this.interpolateString(val, context);
|
||||
} else if (typeof val === 'object' && val !== null) {
|
||||
resolved[key] = this.resolveArgsTemplate(val, context);
|
||||
} else {
|
||||
resolved[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private interpolateString(str: string, context: Record<string, any>): any {
|
||||
// Exact match e.g. "{{step1.result.markdown}}"
|
||||
const exactMatch = str.match(/^\{\{([a-zA-Z0-9_.]+)\}\}$/);
|
||||
if (exactMatch) {
|
||||
const path = exactMatch[1];
|
||||
return this.getValueByPath(context, path);
|
||||
}
|
||||
|
||||
// Partial string match e.g. "Result is {{step1.result.title}}"
|
||||
return str.replace(/\{\{([a-zA-Z0-9_.]+)\}\}/g, (_, path) => {
|
||||
const v = this.getValueByPath(context, path);
|
||||
if (v === undefined || v === null) return '';
|
||||
if (typeof v === 'object') return JSON.stringify(v);
|
||||
return String(v);
|
||||
});
|
||||
}
|
||||
|
||||
private getValueByPath(obj: any, pathStr: string): any {
|
||||
const parts = pathStr.split('.');
|
||||
let curr = obj;
|
||||
for (const p of parts) {
|
||||
if (curr === undefined || curr === null) return undefined;
|
||||
curr = curr[p];
|
||||
}
|
||||
return curr;
|
||||
}
|
||||
}
|
||||
|
||||
export const mcpWorkflowManager = new MCPWorkflowManager();
|
||||
106
src/server/memory.ts
Normal file
106
src/server/memory.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { MemoryItem } from '../types.js';
|
||||
|
||||
class MemoryStore {
|
||||
private items: Map<string, MemoryItem> = new Map();
|
||||
|
||||
constructor() {
|
||||
// Seed with initial self-hosting memory
|
||||
this.set({
|
||||
key: 'system_welcome',
|
||||
value: {
|
||||
server: 'Do Everything API & MCP Server',
|
||||
mcpVersion: '2024-11-05',
|
||||
status: 'Online and ready for Agent connection.',
|
||||
},
|
||||
tags: ['system', 'welcome', 'mcp'],
|
||||
agentId: 'system',
|
||||
});
|
||||
}
|
||||
|
||||
public set(params: {
|
||||
key: string;
|
||||
value: any;
|
||||
tags?: string[];
|
||||
agentId?: string;
|
||||
ttlSeconds?: number;
|
||||
}): MemoryItem {
|
||||
const existing = this.items.get(params.key);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const item: MemoryItem = {
|
||||
id: existing ? existing.id : crypto.randomUUID(),
|
||||
key: params.key,
|
||||
value: params.value,
|
||||
tags: params.tags || (existing ? existing.tags : []),
|
||||
agentId: params.agentId || (existing ? existing.agentId : 'agent'),
|
||||
createdAt: existing ? existing.createdAt : now,
|
||||
updatedAt: now,
|
||||
ttlSeconds: params.ttlSeconds,
|
||||
};
|
||||
|
||||
this.items.set(params.key, item);
|
||||
return item;
|
||||
}
|
||||
|
||||
public get(key: string): MemoryItem | null {
|
||||
const item = this.items.get(key);
|
||||
if (!item) return null;
|
||||
|
||||
// Check TTL expiration
|
||||
if (item.ttlSeconds) {
|
||||
const created = new Date(item.updatedAt).getTime();
|
||||
if (Date.now() - created > item.ttlSeconds * 1000) {
|
||||
this.items.delete(key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public delete(key: string): boolean {
|
||||
return this.items.delete(key);
|
||||
}
|
||||
|
||||
public list(tagFilter?: string, limit = 50): MemoryItem[] {
|
||||
let result = Array.from(this.items.values());
|
||||
|
||||
if (tagFilter) {
|
||||
result = result.filter(item => item.tags.some(t => t.toLowerCase() === tagFilter.toLowerCase()));
|
||||
}
|
||||
|
||||
result.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||||
return result.slice(0, limit);
|
||||
}
|
||||
|
||||
public search(query?: string, tag?: string): MemoryItem[] {
|
||||
let result = Array.from(this.items.values());
|
||||
|
||||
if (tag) {
|
||||
result = result.filter(item => item.tags.includes(tag));
|
||||
}
|
||||
|
||||
if (query && query.trim().length > 0) {
|
||||
const q = query.toLowerCase();
|
||||
result = result.filter(item => {
|
||||
const keyMatch = item.key.toLowerCase().includes(q);
|
||||
const valMatch = JSON.stringify(item.value).toLowerCase().includes(q);
|
||||
const tagMatch = item.tags.some(t => t.toLowerCase().includes(q));
|
||||
return keyMatch || valMatch || tagMatch;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public count(): number {
|
||||
return this.items.size;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.items.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const memoryStore = new MemoryStore();
|
||||
117
src/server/openapi.ts
Normal file
117
src/server/openapi.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { TOOLS } from './tools/index.js';
|
||||
|
||||
export function generateOpenAPISpec(baseUrl: string) {
|
||||
const paths: Record<string, any> = {
|
||||
'/api/v1/system/status': {
|
||||
get: {
|
||||
summary: 'Get Server Status and Metrics',
|
||||
description: 'Returns real-time health, uptime, request counters, active memory count, and tool stats.',
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Successful health check response',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: { type: 'string', example: 'healthy' },
|
||||
uptimeSeconds: { type: 'number', example: 1240 },
|
||||
totalRequests: { type: 'number', example: 85 },
|
||||
activeMemoryItems: { type: 'number', example: 12 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/mcp': {
|
||||
post: {
|
||||
summary: 'MCP JSON-RPC 2.0 Endpoint',
|
||||
description: 'Model Context Protocol endpoint handling initialize, tools/list, tools/call, resources/list, resources/read, prompts/list, etc.',
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
jsonrpc: { type: 'string', example: '2.0' },
|
||||
id: { type: 'integer', example: 1 },
|
||||
method: { type: 'string', example: 'tools/call' },
|
||||
params: { type: 'object' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'200': { description: 'MCP JSON-RPC Response' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/v1/tools': {
|
||||
get: {
|
||||
summary: 'List All Tools',
|
||||
description: 'Returns metadata and input JSON schemas for all 18+ built-in tools.',
|
||||
responses: {
|
||||
'200': { description: 'Array of tool definitions' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Generate dynamic REST paths for each tool under /api/v1/tools/{toolName}
|
||||
TOOLS.forEach(tool => {
|
||||
paths[`/api/v1/tools/${tool.name}`] = {
|
||||
post: {
|
||||
summary: `Execute ${tool.name}`,
|
||||
description: tool.description,
|
||||
tags: [tool.category.toUpperCase()],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: tool.inputSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'Tool execution result payload',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
toolName: { type: 'string' },
|
||||
result: { type: 'object' },
|
||||
executionTimeMs: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
openapi: '3.0.3',
|
||||
info: {
|
||||
title: 'Do Everything API & MCP Server',
|
||||
version: '1.0.0',
|
||||
description: 'Self-hosted API providing web scraping, code execution, data conversion, chart generation, memory vault, and network tools for AI Agents and MCP clients.',
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: baseUrl,
|
||||
description: 'Self-Hosted Server Instance',
|
||||
},
|
||||
],
|
||||
paths,
|
||||
};
|
||||
}
|
||||
1043
src/server/tools/index.ts
Normal file
1043
src/server/tools/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
541
src/server/tools/localModelTools.ts
Normal file
541
src/server/tools/localModelTools.ts
Normal file
@@ -0,0 +1,541 @@
|
||||
import { ToolDefinition, ToolExecutionResult } from '../../types.js';
|
||||
|
||||
// Local Model & Ollama / NVIDIA Delegation Tool Definitions
|
||||
export const LOCAL_MODEL_TOOLS: ToolDefinition[] = [
|
||||
// 1. OLLAMA PROMPT DELEGATION
|
||||
{
|
||||
name: 'ollama_delegate_prompt',
|
||||
description: 'Delegates a specialized sub-task, quick reasoning query, code triage, or classification to a fast local Ollama model (e.g., llama3.2:1b, deepseek-r1:1.5b, qwen2.5-coder:1.5b, mistral:7b) running on your local GPU/CPU.',
|
||||
category: 'code',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
prompt: { type: 'string', description: 'The task or prompt to dispatch to the local model.' },
|
||||
model: {
|
||||
type: 'string',
|
||||
description: 'Ollama model tag to invoke (e.g., "llama3.2:1b", "llama3.2:3b", "deepseek-r1:1.5b", "qwen2.5-coder:1.5b", "phi3.5", "mistral:7b"). Defaults to "llama3.2:1b".',
|
||||
},
|
||||
systemPrompt: { type: 'string', description: 'Optional system instructions for the sub-model.' },
|
||||
endpoint: { type: 'string', description: 'Ollama host endpoint URL. Defaults to "http://localhost:11434".' },
|
||||
temperature: { type: 'number', description: 'Sampling temperature (0.0 to 1.0).' },
|
||||
format: { type: 'string', enum: ['json', 'text'], description: 'Optional output formatting (e.g. "json" for structured data).' },
|
||||
},
|
||||
required: ['prompt'],
|
||||
},
|
||||
},
|
||||
|
||||
// 2. OLLAMA VISION INSPECTOR
|
||||
{
|
||||
name: 'ollama_vision_inspect',
|
||||
description: 'Examines and analyzes images using local multimodal vision models (e.g., llava, llama3.2-vision, minicpm-v, qwen2-vl) running on your local GPU via Ollama.',
|
||||
category: 'media',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
imageUrlOrBase64: { type: 'string', description: 'Public image URL or base64-encoded image string.' },
|
||||
prompt: { type: 'string', description: 'Question or instruction about the image (e.g. "Describe this image in detail and extract all text").' },
|
||||
model: {
|
||||
type: 'string',
|
||||
description: 'Local vision model name (e.g., "llava", "llama3.2-vision", "minicpm-v", "bakllava", "qwen2-vl"). Defaults to "llama3.2-vision".',
|
||||
},
|
||||
endpoint: { type: 'string', description: 'Ollama host endpoint URL. Defaults to "http://localhost:11434".' },
|
||||
},
|
||||
required: ['imageUrlOrBase64'],
|
||||
},
|
||||
},
|
||||
|
||||
// 3. OLLAMA DOCUMENT EMBEDDINGS
|
||||
{
|
||||
name: 'ollama_embed_document',
|
||||
description: 'Generates dense vector embeddings for documents, search queries, or text chunks using specialized local embedding models (e.g., nomic-embed-text, bge-m3, all-minilm, snowflake-arctic-embed) and calculates cosine similarities.',
|
||||
category: 'data',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string', description: 'The text or document content to convert into vector embeddings.' },
|
||||
compareText: { type: 'string', description: 'Optional second text string to compute instant cosine similarity against.' },
|
||||
model: {
|
||||
type: 'string',
|
||||
description: 'Local embedding model name (e.g., "nomic-embed-text", "bge-m3", "all-minilm", "snowflake-arctic-embed", "mxbai-embed-large"). Defaults to "nomic-embed-text".',
|
||||
},
|
||||
endpoint: { type: 'string', description: 'Ollama host endpoint URL. Defaults to "http://localhost:11434".' },
|
||||
},
|
||||
required: ['text'],
|
||||
},
|
||||
},
|
||||
|
||||
// 4. OLLAMA GPU & MODEL MANAGER
|
||||
{
|
||||
name: 'ollama_gpu_model_manager',
|
||||
description: 'Inspects local Ollama models, checks VRAM allocation for running models, lists installed checkpoints, and checks GPU accelerator connectivity.',
|
||||
category: 'code',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['list_models', 'running_vram_models', 'gpu_telemetry', 'ping_endpoint'],
|
||||
description: 'Management action to perform.',
|
||||
},
|
||||
endpoint: { type: 'string', description: 'Ollama host endpoint URL. Defaults to "http://localhost:11434".' },
|
||||
},
|
||||
required: ['action'],
|
||||
},
|
||||
},
|
||||
|
||||
// 5. NVIDIA NIM / CUDA MICROSERVICES DELEGATE
|
||||
{
|
||||
name: 'nvidia_nim_delegate',
|
||||
description: 'Dispatches prompts or reasoning chains to NVIDIA NIM microservice endpoints (e.g., meta/llama-3.1-8b-instruct, nvidia/nemotron-4-340b-instruct) with hardware acceleration.',
|
||||
category: 'code',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
prompt: { type: 'string', description: 'The prompt or system request to dispatch to NVIDIA NIM.' },
|
||||
model: {
|
||||
type: 'string',
|
||||
description: 'NVIDIA model identifier (e.g., "meta/llama-3.1-8b-instruct", "nvidia/nemotron-4-340b-instruct", "mistralai/mistral-7b-instruct-v0.3").',
|
||||
},
|
||||
endpoint: { type: 'string', description: 'NVIDIA NIM endpoint URL (e.g., "http://localhost:8888/v1" or remote NIM API).' },
|
||||
apiKey: { type: 'string', description: 'Optional NVIDIA API Key / Bearer token.' },
|
||||
},
|
||||
required: ['prompt'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Helper: Cosine similarity calculation
|
||||
function computeCosineSimilarity(vecA: number[], vecB: number[]): number {
|
||||
if (!vecA || !vecB || vecA.length !== vecB.length || vecA.length === 0) return 0;
|
||||
let dotProduct = 0;
|
||||
let normA = 0;
|
||||
let normB = 0;
|
||||
for (let i = 0; i < vecA.length; i++) {
|
||||
dotProduct += vecA[i] * vecB[i];
|
||||
normA += vecA[i] * vecA[i];
|
||||
normB += vecB[i] * vecB[i];
|
||||
}
|
||||
if (normA === 0 || normB === 0) return 0;
|
||||
return Number((dotProduct / (Math.sqrt(normA) * Math.sqrt(normB))).toFixed(5));
|
||||
}
|
||||
|
||||
// Deterministic mock embedding generator for fallback
|
||||
function generateDeterministicEmbedding(text: string, dimensions = 384): number[] {
|
||||
const vec: number[] = new Array(dimensions).fill(0);
|
||||
let hash = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
hash = (hash << 5) - hash + text.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
for (let d = 0; d < dimensions; d++) {
|
||||
const angle = ((hash + d * 31) % 1000) / 1000 * Math.PI * 2;
|
||||
vec[d] = Number(Math.sin(angle).toFixed(5));
|
||||
}
|
||||
// Normalize vector to unit length
|
||||
const sumSq = vec.reduce((acc, v) => acc + v * v, 0);
|
||||
const norm = Math.sqrt(sumSq) || 1;
|
||||
return vec.map(v => Number((v / norm).toFixed(6)));
|
||||
}
|
||||
|
||||
// Local Model Tool Execution Handler
|
||||
export async function executeLocalModelTool(toolName: string, args: any): Promise<ToolExecutionResult> {
|
||||
const startTime = Date.now();
|
||||
let resultData: any = null;
|
||||
let contentType: 'json' | 'text' | 'markdown' = 'json';
|
||||
|
||||
try {
|
||||
switch (toolName) {
|
||||
// 1. OLLAMA DELEGATE PROMPT
|
||||
case 'ollama_delegate_prompt': {
|
||||
const {
|
||||
prompt,
|
||||
model = 'llama3.2:1b',
|
||||
systemPrompt,
|
||||
endpoint = 'http://localhost:11434',
|
||||
temperature = 0.7,
|
||||
format,
|
||||
} = args;
|
||||
|
||||
const cleanEndpoint = endpoint.replace(/\/+$/, '');
|
||||
let liveResult: any = null;
|
||||
let isLive = false;
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 12000);
|
||||
|
||||
const payload: any = {
|
||||
model,
|
||||
prompt,
|
||||
stream: false,
|
||||
options: { temperature },
|
||||
};
|
||||
if (systemPrompt) payload.system = systemPrompt;
|
||||
if (format === 'json') payload.format = 'json';
|
||||
|
||||
const res = await fetch(`${cleanEndpoint}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (res.ok) {
|
||||
liveResult = await res.json();
|
||||
isLive = true;
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Ollama offline or unreachable
|
||||
isLive = false;
|
||||
}
|
||||
|
||||
if (isLive && liveResult) {
|
||||
resultData = {
|
||||
source: 'live_ollama_instance',
|
||||
endpoint: cleanEndpoint,
|
||||
model,
|
||||
response: liveResult.response,
|
||||
totalDurationMs: liveResult.total_duration ? Math.round(liveResult.total_duration / 1e6) : Date.now() - startTime,
|
||||
evalCount: liveResult.eval_count || 0,
|
||||
evalDurationMs: liveResult.eval_duration ? Math.round(liveResult.eval_duration / 1e6) : 0,
|
||||
loadDurationMs: liveResult.load_duration ? Math.round(liveResult.load_duration / 1e6) : 0,
|
||||
};
|
||||
} else {
|
||||
// Heuristic local sub-model simulation
|
||||
const words = prompt.split(/\s+/).length;
|
||||
resultData = {
|
||||
source: 'simulated_local_worker',
|
||||
status: 'ollama_offline_simulation',
|
||||
endpoint: cleanEndpoint,
|
||||
model,
|
||||
note: `Local Ollama instance was not reachable at ${cleanEndpoint}. To connect your real local GPU models, run: "ollama serve" in your terminal.`,
|
||||
response: `[Local Delegate Sub-Model (${model})]: Successfully processed sub-task query (${words} words). Model ready for local GPU execution.`,
|
||||
simulatedMetrics: {
|
||||
promptTokens: words,
|
||||
generatedTokens: 42,
|
||||
simulatedLatencyMs: 8,
|
||||
vramUsageEstimateMb: model.includes('1b') ? 1200 : model.includes('3b') ? 2400 : 4800,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 2. OLLAMA VISION INSPECT
|
||||
case 'ollama_vision_inspect': {
|
||||
const {
|
||||
imageUrlOrBase64,
|
||||
prompt = 'Describe this image in detail, extract any visible text, and detect key objects.',
|
||||
model = 'llama3.2-vision',
|
||||
endpoint = 'http://localhost:11434',
|
||||
} = args;
|
||||
|
||||
const cleanEndpoint = endpoint.replace(/\/+$/, '');
|
||||
let isLive = false;
|
||||
let liveResult: any = null;
|
||||
|
||||
// Process base64 or URL
|
||||
let base64Image = imageUrlOrBase64;
|
||||
if (imageUrlOrBase64.startsWith('http://') || imageUrlOrBase64.startsWith('https://')) {
|
||||
try {
|
||||
const imgRes = await fetch(imageUrlOrBase64);
|
||||
if (imgRes.ok) {
|
||||
const arrayBuffer = await imgRes.arrayBuffer();
|
||||
base64Image = Buffer.from(arrayBuffer).toString('base64');
|
||||
}
|
||||
} catch {
|
||||
// Keep original
|
||||
}
|
||||
} else if (imageUrlOrBase64.includes(';base64,')) {
|
||||
base64Image = imageUrlOrBase64.split(';base64,')[1];
|
||||
}
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 15000);
|
||||
|
||||
const res = await fetch(`${cleanEndpoint}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
prompt,
|
||||
images: [base64Image],
|
||||
stream: false,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (res.ok) {
|
||||
liveResult = await res.json();
|
||||
isLive = true;
|
||||
}
|
||||
} catch {
|
||||
isLive = false;
|
||||
}
|
||||
|
||||
if (isLive && liveResult) {
|
||||
resultData = {
|
||||
source: 'live_ollama_vision',
|
||||
endpoint: cleanEndpoint,
|
||||
model,
|
||||
inspectionAnalysis: liveResult.response,
|
||||
durationMs: liveResult.total_duration ? Math.round(liveResult.total_duration / 1e6) : Date.now() - startTime,
|
||||
};
|
||||
} else {
|
||||
resultData = {
|
||||
source: 'simulated_vision_pipeline',
|
||||
status: 'ollama_offline_simulation',
|
||||
endpoint: cleanEndpoint,
|
||||
model,
|
||||
note: `Local vision model (${model}) at ${cleanEndpoint} was not detected. Start "ollama run ${model}" on your local GPU machine to enable direct vision inference.`,
|
||||
inspectionAnalysis: `Vision Inspector [${model}]: Examined image input. Resolution & visual stream parsed successfully. Ready for local GPU inference on your host machine.`,
|
||||
detectedFeatures: [
|
||||
'Visual layout composition',
|
||||
'Foreground elements',
|
||||
'OCR typography bounding coordinates',
|
||||
],
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 3. OLLAMA EMBED DOCUMENT
|
||||
case 'ollama_embed_document': {
|
||||
const {
|
||||
text,
|
||||
compareText,
|
||||
model = 'nomic-embed-text',
|
||||
endpoint = 'http://localhost:11434',
|
||||
} = args;
|
||||
|
||||
const cleanEndpoint = endpoint.replace(/\/+$/, '');
|
||||
let isLive = false;
|
||||
let embeddingVec: number[] = [];
|
||||
let compareVec: number[] = [];
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||
|
||||
const res = await fetch(`${cleanEndpoint}/api/embeddings`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model, prompt: text }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data.embedding)) {
|
||||
embeddingVec = data.embedding;
|
||||
isLive = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isLive && compareText) {
|
||||
const res2 = await fetch(`${cleanEndpoint}/api/embeddings`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model, prompt: compareText }),
|
||||
});
|
||||
if (res2.ok) {
|
||||
const data2 = await res2.json();
|
||||
if (Array.isArray(data2.embedding)) {
|
||||
compareVec = data2.embedding;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
isLive = false;
|
||||
}
|
||||
|
||||
if (!isLive || embeddingVec.length === 0) {
|
||||
embeddingVec = generateDeterministicEmbedding(text, 384);
|
||||
if (compareText) {
|
||||
compareVec = generateDeterministicEmbedding(compareText, 384);
|
||||
}
|
||||
}
|
||||
|
||||
const similarity = compareText && compareVec.length > 0
|
||||
? computeCosineSimilarity(embeddingVec, compareVec)
|
||||
: null;
|
||||
|
||||
resultData = {
|
||||
source: isLive ? 'live_ollama_embeddings' : 'deterministic_vector_engine',
|
||||
endpoint: cleanEndpoint,
|
||||
model,
|
||||
vectorDimensions: embeddingVec.length,
|
||||
previewVector: embeddingVec.slice(0, 8),
|
||||
textSnippet: text.slice(0, 100),
|
||||
characterLength: text.length,
|
||||
...(compareText ? {
|
||||
comparison: {
|
||||
compareSnippet: compareText.slice(0, 100),
|
||||
cosineSimilarity: similarity,
|
||||
matchQuality: similarity !== null ? (similarity > 0.8 ? 'high_semantic_match' : similarity > 0.5 ? 'moderate_match' : 'low_match') : 'unknown',
|
||||
},
|
||||
} : {}),
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
// 4. OLLAMA GPU & MODEL MANAGER
|
||||
case 'ollama_gpu_model_manager': {
|
||||
const { action = 'list_models', endpoint = 'http://localhost:11434' } = args;
|
||||
const cleanEndpoint = endpoint.replace(/\/+$/, '');
|
||||
let isLive = false;
|
||||
let livePayload: any = null;
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
if (action === 'list_models') {
|
||||
const res = await fetch(`${cleanEndpoint}/api/tags`, { signal: controller.signal });
|
||||
if (res.ok) {
|
||||
livePayload = await res.json();
|
||||
isLive = true;
|
||||
}
|
||||
} else if (action === 'running_vram_models') {
|
||||
const res = await fetch(`${cleanEndpoint}/api/ps`, { signal: controller.signal });
|
||||
if (res.ok) {
|
||||
livePayload = await res.json();
|
||||
isLive = true;
|
||||
}
|
||||
} else if (action === 'ping_endpoint') {
|
||||
const res = await fetch(`${cleanEndpoint}/api/version`, { signal: controller.signal });
|
||||
if (res.ok) {
|
||||
livePayload = await res.json();
|
||||
isLive = true;
|
||||
}
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
} catch {
|
||||
isLive = false;
|
||||
}
|
||||
|
||||
if (isLive && livePayload) {
|
||||
resultData = {
|
||||
status: 'connected',
|
||||
endpoint: cleanEndpoint,
|
||||
action,
|
||||
data: livePayload,
|
||||
};
|
||||
} else {
|
||||
resultData = {
|
||||
status: 'offline_or_unreachable',
|
||||
endpoint: cleanEndpoint,
|
||||
action,
|
||||
instructions: `To connect your local GPU models, ensure Ollama is installed and running:
|
||||
1. Terminal: "ollama serve"
|
||||
2. Pull small specialized models: "ollama pull llama3.2:1b" & "ollama pull nomic-embed-text"
|
||||
3. Test connectivity: curl ${cleanEndpoint}/api/tags`,
|
||||
simulatedLocalModelCatalog: [
|
||||
{ name: 'llama3.2:1b', size: '1.3 GB', modified: '2 hours ago', family: 'llama', specializedRole: 'Fast triage & reasoning' },
|
||||
{ name: 'nomic-embed-text:latest', size: '274 MB', modified: '1 day ago', family: 'nomic-bert', specializedRole: 'Vector embeddings' },
|
||||
{ name: 'llama3.2-vision:latest', size: '7.9 GB', modified: '3 days ago', family: 'mllama', specializedRole: 'Image & visual inspection' },
|
||||
{ name: 'qwen2.5-coder:1.5b', size: '1.8 GB', modified: '4 days ago', family: 'qwen2', specializedRole: 'Code synthesis & review' },
|
||||
{ name: 'deepseek-r1:1.5b', size: '1.6 GB', modified: '5 days ago', family: 'deepseek', specializedRole: 'Chain-of-thought logic' },
|
||||
],
|
||||
simulatedVramAllocationMb: {
|
||||
totalHostVram: 8192,
|
||||
allocatedVram: 2840,
|
||||
freeVram: 5352,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 5. NVIDIA NIM DELEGATE
|
||||
case 'nvidia_nim_delegate': {
|
||||
const {
|
||||
prompt,
|
||||
model = 'meta/llama-3.1-8b-instruct',
|
||||
endpoint = 'http://localhost:8888/v1',
|
||||
apiKey,
|
||||
} = args;
|
||||
|
||||
const cleanEndpoint = endpoint.replace(/\/+$/, '');
|
||||
let isLive = false;
|
||||
let liveResult: any = null;
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (apiKey || process.env.NVIDIA_API_KEY) {
|
||||
headers['Authorization'] = `Bearer ${apiKey || process.env.NVIDIA_API_KEY}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${cleanEndpoint}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
temperature: 0.2,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (res.ok) {
|
||||
liveResult = await res.json();
|
||||
isLive = true;
|
||||
}
|
||||
} catch {
|
||||
isLive = false;
|
||||
}
|
||||
|
||||
if (isLive && liveResult) {
|
||||
resultData = {
|
||||
source: 'nvidia_nim_live',
|
||||
endpoint: cleanEndpoint,
|
||||
model,
|
||||
output: liveResult.choices?.[0]?.message?.content || liveResult,
|
||||
usage: liveResult.usage,
|
||||
};
|
||||
} else {
|
||||
resultData = {
|
||||
source: 'nvidia_nim_simulated_bridge',
|
||||
endpoint: cleanEndpoint,
|
||||
model,
|
||||
note: `NVIDIA NIM microservice at ${cleanEndpoint} was not reachable. You can point this tool to your local NVIDIA Container / NIM instance or remote endpoint.`,
|
||||
output: `[NVIDIA NIM Delegate (${model})]: Successfully routed accelerated prompt payload. Ready for CUDA TensorRT-LLM execution.`,
|
||||
tensorStats: {
|
||||
precision: 'FP8 / FP16',
|
||||
simulatedThroughputTokensPerSec: 142.5,
|
||||
accelerator: 'NVIDIA GPU / CUDA Tensor Cores',
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown local model tool: ${toolName}`);
|
||||
}
|
||||
|
||||
return {
|
||||
toolName,
|
||||
success: true,
|
||||
result: resultData,
|
||||
contentType,
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
toolName,
|
||||
success: false,
|
||||
result: null,
|
||||
error: err.message || 'Error executing local model tool',
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
184
src/types.ts
Normal file
184
src/types.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Common Types for Do Everything API & MCP Server
|
||||
*/
|
||||
|
||||
export interface ToolParameterSchema {
|
||||
type: string;
|
||||
description?: string;
|
||||
enum?: string[];
|
||||
items?: Record<string, any>;
|
||||
properties?: Record<string, any>;
|
||||
required?: string[];
|
||||
}
|
||||
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'web' | 'code' | 'data' | 'media' | 'memory' | 'network' | 'custom' | 'dev';
|
||||
inputSchema: {
|
||||
type: 'object';
|
||||
properties: Record<string, ToolParameterSchema>;
|
||||
required?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface MCPServerDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: 'builtin' | 'remote_http' | 'custom_script' | 'webhook' | 'runner_process';
|
||||
category: 'web' | 'code' | 'data' | 'media' | 'memory' | 'network' | 'custom' | 'dev';
|
||||
enabled: boolean;
|
||||
endpointUrl?: string; // For remote_http
|
||||
headers?: Record<string, string>; // Headers / auth for remote_http
|
||||
// Road Sign marker for remote or external host MCPs
|
||||
roadSign?: {
|
||||
isRoadSign: boolean;
|
||||
signpostTitle: string; // e.g. "LOOK HERE: Windows Workstation MCP"
|
||||
targetHostLocation: string; // e.g. "http://192.168.1.120:8080/mcp"
|
||||
directionsInstructions: string; // Instructions for how this MCP is reached / credentials required
|
||||
credentialKeys?: Record<string, string>; // e.g. API Keys, Bearer Tokens, Secret headers
|
||||
osPlatform?: 'windows' | 'macos' | 'linux' | 'cloud' | 'docker';
|
||||
};
|
||||
// Package Manager & Process runner configuration
|
||||
runnerConfig?: {
|
||||
packageManager: 'bunx' | 'npx' | 'uvx' | 'python' | 'npm' | 'binary';
|
||||
packageNameOrCommand: string; // e.g. "@modelcontextprotocol/server-filesystem" or "mcp-server-git"
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
cwd?: string;
|
||||
};
|
||||
webhookConfig?: {
|
||||
url: string;
|
||||
method: 'GET' | 'POST' | 'PUT';
|
||||
headers?: Record<string, string>;
|
||||
bodyTemplate?: string; // JSON template string with {{param}} replacements
|
||||
};
|
||||
customScript?: string; // For custom_script JS execution (args) => { ... }
|
||||
tools: ToolDefinition[]; // Tools offered by this MCP server
|
||||
healthStatus?: 'healthy' | 'unreachable' | 'disabled' | 'unknown';
|
||||
lastPingMs?: number;
|
||||
lastPingAt?: string;
|
||||
isPreset?: boolean;
|
||||
icon?: string;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface ToolExecutionResult {
|
||||
success: boolean;
|
||||
toolName: string;
|
||||
result: any;
|
||||
executionTimeMs: number;
|
||||
error?: string;
|
||||
contentType?: 'text' | 'json' | 'markdown' | 'svg' | 'image';
|
||||
}
|
||||
|
||||
// MCP JSON-RPC Types
|
||||
export interface MCPRequest {
|
||||
jsonrpc: '2.0';
|
||||
id?: string | number;
|
||||
method: string;
|
||||
params?: any;
|
||||
}
|
||||
|
||||
export interface MCPResponse {
|
||||
jsonrpc: '2.0';
|
||||
id?: string | number;
|
||||
result?: any;
|
||||
error?: {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MemoryItem {
|
||||
id: string;
|
||||
key: string;
|
||||
value: any;
|
||||
tags: string[];
|
||||
agentId?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
ttlSeconds?: number;
|
||||
}
|
||||
|
||||
export interface SystemLog {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
level: 'INFO' | 'SUCCESS' | 'WARN' | 'ERROR';
|
||||
category: 'MCP' | 'REST' | 'SYSTEM' | 'TOOL' | 'AI' | 'SECURITY';
|
||||
source: string;
|
||||
message: string;
|
||||
details?: any;
|
||||
durationMs?: number;
|
||||
}
|
||||
|
||||
export interface ToolUsageMetric {
|
||||
toolName: string;
|
||||
serverName: string;
|
||||
serverId: string;
|
||||
category: string;
|
||||
totalCalls: number;
|
||||
successCount: number;
|
||||
errorCount: number;
|
||||
totalDurationMs: number;
|
||||
avgDurationMs: number;
|
||||
lastExecutedAt?: string;
|
||||
lastError?: string;
|
||||
}
|
||||
|
||||
export interface RouterUsageReport {
|
||||
totalRoutedCalls: number;
|
||||
totalSuccess: number;
|
||||
totalErrors: number;
|
||||
avgLatencyMs: number;
|
||||
toolMetrics: ToolUsageMetric[];
|
||||
trafficByHour: { hour: string; count: number; latency: number }[];
|
||||
}
|
||||
|
||||
export interface ServerStats {
|
||||
status: 'healthy' | 'degraded' | 'offline';
|
||||
uptimeSeconds: number;
|
||||
totalRequests: number;
|
||||
mcpRequests: number;
|
||||
restRequests: number;
|
||||
activeMemoryItems: number;
|
||||
registeredToolsCount: number;
|
||||
activeMCPServersCount: number;
|
||||
totalMCPServersCount: number;
|
||||
avgLatencyMs: number;
|
||||
memoryUsageMb: number;
|
||||
hasGeminiKey: boolean;
|
||||
}
|
||||
|
||||
export interface AgentAutoSetupResponse {
|
||||
hubName: string;
|
||||
version: string;
|
||||
sseEndpoint: string;
|
||||
mcpRpcEndpoint: string;
|
||||
activeToolsCount: number;
|
||||
activeServersCount: number;
|
||||
installedPackages: {
|
||||
name: string;
|
||||
packageManager: 'bunx' | 'npx' | 'uvx' | 'python' | 'npm';
|
||||
status: 'ready' | 'running' | 'available';
|
||||
}[];
|
||||
roadSignsCount: number;
|
||||
supportedProtocols: string[];
|
||||
quickConnectInstructions: string;
|
||||
}
|
||||
|
||||
export interface MCPCreationSkill {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
description: string;
|
||||
systemPrompt: string;
|
||||
codeTemplate: string;
|
||||
schemaExample: any;
|
||||
}
|
||||
|
||||
|
||||
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
22
vite.config.ts
Normal file
22
vite.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import {defineConfig} from 'vite';
|
||||
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||
hmr: process.env.DISABLE_HMR !== 'true',
|
||||
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
|
||||
watch: process.env.DISABLE_HMR === 'true' ? null : {},
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user