Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a9c5780c3 | |||
| fbfc9feab0 | |||
| 8486261555 | |||
| 5e1f2669e6 | |||
| 541f1c17b7 | |||
| 048fafa3db | |||
| 77e2c4f9b6 | |||
| 4eb1aaf640 | |||
| 61f2f83e0c | |||
| 2dc61e4937 |
5
.changeset/next-config-server-actions.md
Normal file
5
.changeset/next-config-server-actions.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@mintel/next-config": patch
|
||||
---
|
||||
|
||||
fix: add serverActions.allowedOrigins to support branch deployments
|
||||
6
.env
6
.env
@@ -3,6 +3,7 @@ IMAGE_TAG=v1.8.19
|
||||
PROJECT_NAME=at-mintel
|
||||
PROJECT_COLOR=#82ed20
|
||||
GITEA_TOKEN=ccce002e30fe16a31a6c9d5a414740af2f72a582
|
||||
GITEA_HOST=https://git.infra.mintel.me
|
||||
OPENROUTER_API_KEY=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
|
||||
ZYTE_API_KEY=1f0f74726f044f55aaafc7ead32cd489
|
||||
REPLICATE_API_KEY=r8_W3grtpXMRfi0u3AM9VdkKbuWdZMmhwU2Tn0yt
|
||||
@@ -11,6 +12,11 @@ DATA_FOR_SEO_API_KEY=bWFyY0BtaW50ZWwubWU6MjQ0YjBjZmIzOGY3NTIzZA==
|
||||
DATA_FOR_SEO_LOGIN=marc@mintel.me
|
||||
DATA_FOR_SEO_PASSWORD=244b0cfb38f7523d
|
||||
|
||||
# Kabelfachmann LLM Configuration
|
||||
KABELFACHMANN_LLM_PROVIDER=openrouter
|
||||
KABELFACHMANN_OLLAMA_MODEL=qwen3.5
|
||||
KABELFACHMANN_OLLAMA_HOST=http://host.docker.internal:11434
|
||||
|
||||
# Authentication
|
||||
GATEKEEPER_PASSWORD=mintel
|
||||
AUTH_COOKIE_NAME=mintel_gatekeeper_session
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -51,3 +51,7 @@ apps/web/out/estimations/
|
||||
# Memory MCP
|
||||
data/qdrant/
|
||||
packages/memory-mcp/models/
|
||||
|
||||
# Kabelfachmann MCP
|
||||
packages/kabelfachmann-mcp/data/
|
||||
packages/kabelfachmann-mcp/models/
|
||||
@@ -3,14 +3,88 @@ services:
|
||||
image: qdrant/qdrant:latest
|
||||
container_name: qdrant-mcp
|
||||
ports:
|
||||
- "6333:6333"
|
||||
- "6334:6334"
|
||||
- "6335:6333"
|
||||
- "6336:6334"
|
||||
volumes:
|
||||
- ./data/qdrant:/qdrant/storage
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcp-network
|
||||
|
||||
gitea-mcp:
|
||||
build:
|
||||
context: ./packages/gitea-mcp
|
||||
container_name: gitea-mcp
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "3001:3001"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcp-network
|
||||
|
||||
memory-mcp:
|
||||
build:
|
||||
context: ./packages/memory-mcp
|
||||
container_name: memory-mcp
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "3002:3002"
|
||||
depends_on:
|
||||
- qdrant
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcp-network
|
||||
|
||||
umami-mcp:
|
||||
build:
|
||||
context: ./packages/umami-mcp
|
||||
container_name: umami-mcp
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "3003:3003"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcp-network
|
||||
|
||||
serpbear-mcp:
|
||||
build:
|
||||
context: ./packages/serpbear-mcp
|
||||
container_name: serpbear-mcp
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "3004:3004"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcp-network
|
||||
|
||||
glitchtip-mcp:
|
||||
build:
|
||||
context: ./packages/glitchtip-mcp
|
||||
container_name: glitchtip-mcp
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "3005:3005"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcp-network
|
||||
|
||||
klz-payload-mcp:
|
||||
build:
|
||||
context: ./packages/klz-payload-mcp
|
||||
container_name: klz-payload-mcp
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "3006:3006"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mcp-network
|
||||
|
||||
networks:
|
||||
mcp-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'gitea-mcp',
|
||||
script: 'node',
|
||||
args: 'dist/start.js',
|
||||
cwd: './packages/gitea-mcp',
|
||||
watch: false,
|
||||
},
|
||||
{
|
||||
name: 'memory-mcp',
|
||||
script: 'node',
|
||||
args: 'dist/start.js',
|
||||
cwd: './packages/memory-mcp',
|
||||
watch: false,
|
||||
},
|
||||
{
|
||||
name: 'umami-mcp',
|
||||
script: 'node',
|
||||
args: 'dist/start.js',
|
||||
cwd: './packages/umami-mcp',
|
||||
watch: false,
|
||||
},
|
||||
{
|
||||
name: 'serpbear-mcp',
|
||||
script: 'node',
|
||||
args: 'dist/start.js',
|
||||
cwd: './packages/serpbear-mcp',
|
||||
watch: false,
|
||||
},
|
||||
{
|
||||
name: 'glitchtip-mcp',
|
||||
script: 'node',
|
||||
args: 'dist/start.js',
|
||||
cwd: './packages/glitchtip-mcp',
|
||||
watch: false,
|
||||
},
|
||||
{
|
||||
name: 'klz-payload-mcp',
|
||||
script: 'node',
|
||||
args: 'dist/start.js',
|
||||
cwd: './packages/klz-payload-mcp',
|
||||
watch: false,
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
69
eslint-errors-2.txt
Normal file
69
eslint-errors-2.txt
Normal file
@@ -0,0 +1,69 @@
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/gitea-mcp/src/index.ts[24m[0m
|
||||
[0m [2m11:0[22m [31merror[39m Parsing error: Identifier expected[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/glitchtip-mcp/src/index.ts[24m[0m
|
||||
[0m [2m124:19[22m [33mwarning[39m 'res' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/klz-payload-mcp/src/index.ts[24m[0m
|
||||
[0m [2m39:18[22m [33mwarning[39m 'e' is defined but never used. Allowed unused caught errors must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/memory-mcp/src/qdrant.test.ts[24m[0m
|
||||
[0m [2m7:52[22m [33mwarning[39m 'text' is defined but never used. Allowed unused args must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/page-audit/src/report.ts[24m[0m
|
||||
[0m [2m7:47[22m [33mwarning[39m 'PageAuditData' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m7:62[22m [33mwarning[39m 'AuditIssue' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/AiFieldButton.tsx[24m[0m
|
||||
[0m [2m11:13[22m [33mwarning[39m 'value' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateSlugButton.tsx[24m[0m
|
||||
[0m [2m20:21[22m [33mwarning[39m 'replaceState' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m21:13[22m [33mwarning[39m 'value' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateThumbnailButton.tsx[24m[0m
|
||||
[0m [2m21:13[22m [33mwarning[39m 'value' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/OptimizeButton.tsx[24m[0m
|
||||
[0m [2m5:10[22m [33mwarning[39m 'Button' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/mcpAdapter.ts[24m[0m
|
||||
[0m [2m44:15[22m [33mwarning[39m 'toolSchema' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/memoryDb.ts[24m[0m
|
||||
[0m [2m89:31[22m [33mwarning[39m 'query' is defined but never used. Allowed unused args must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/payloadLocal.ts[24m[0m
|
||||
[0m [2m3:40[22m [33mwarning[39m 'User' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/types.ts[24m[0m
|
||||
[0m [2m1:15[22m [33mwarning[39m 'Plugin' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/ConceptPDF.tsx[24m[0m
|
||||
[0m [2m4:18[22m [33mwarning[39m 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m5:10[22m [33mwarning[39m 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/EstimationPDF.tsx[24m[0m
|
||||
[0m [2m4:18[22m [33mwarning[39m 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m5:10[22m [33mwarning[39m 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m54:11[22m [33mwarning[39m 'getPageNum' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/InfoPDF.tsx[24m[0m
|
||||
[0m [2m5:13[22m [33mwarning[39m 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m12:5[22m [33mwarning[39m 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SharedUI.tsx[24m[0m
|
||||
[0m [2m528:5[22m [33mwarning[39m 'bankData' is defined but never used. Allowed unused args must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SimpleLayout.tsx[24m[0m
|
||||
[0m [2m4:52[22m [33mwarning[39m 'PDFText' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m5:26[22m [33mwarning[39m 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/seo-engine/src/report.ts[24m[0m
|
||||
[0m [2m5:3[22m [33mwarning[39m 'TopicCluster' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m6:3[22m [33mwarning[39m 'ContentGap' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m7:3[22m [33mwarning[39m 'CompetitorRanking' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[31m[1m✖ 28 problems (1 error, 27 warnings)[22m[39m[0m
|
||||
[0m[31m[1m[22m[39m[0m
|
||||
97
eslint-errors.txt
Normal file
97
eslint-errors.txt
Normal file
@@ -0,0 +1,97 @@
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/gitea-mcp/src/index.ts[24m[0m
|
||||
[0m [2m12:5[22m [33mwarning[39m 'Resource' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m14:10[22m [33mwarning[39m 'z' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m427:30[22m [33mwarning[39m 'e' is defined but never used. Allowed unused caught errors must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m745:50[22m [31merror[39m Unnecessary escape character: \/ [2mno-useless-escape[22m[0m
|
||||
[0m [2m745:60[22m [31merror[39m Unnecessary escape character: \/ [2mno-useless-escape[22m[0m
|
||||
[0m [2m799:54[22m [31merror[39m Unnecessary escape character: \/ [2mno-useless-escape[22m[0m
|
||||
[0m [2m799:64[22m [31merror[39m Unnecessary escape character: \/ [2mno-useless-escape[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/glitchtip-mcp/src/index.ts[24m[0m
|
||||
[0m [2m124:19[22m [33mwarning[39m 'res' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/klz-payload-mcp/src/index.ts[24m[0m
|
||||
[0m [2m39:18[22m [33mwarning[39m 'e' is defined but never used. Allowed unused caught errors must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/memory-mcp/src/qdrant.test.ts[24m[0m
|
||||
[0m [2m7:52[22m [33mwarning[39m 'text' is defined but never used. Allowed unused args must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/page-audit/src/report.ts[24m[0m
|
||||
[0m [2m7:47[22m [33mwarning[39m 'PageAuditData' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m7:62[22m [33mwarning[39m 'AuditIssue' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/chatPlugin.ts[24m[0m
|
||||
[0m [2m1:15[22m [33mwarning[39m 'Config' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m10:17[22m [31merror[39m 'config' is never reassigned. Use 'const' instead [2mprefer-const[22m[0m
|
||||
[0m [2m48:37[22m [33mwarning[39m 'req' is defined but never used. Allowed unused args must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/ChatWindow/index.tsx[24m[0m
|
||||
[0m [2m43:5[22m [31merror[39m Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free [2m@typescript-eslint/ban-ts-comment[22m[0m
|
||||
[0m [2m44:63[22m [33mwarning[39m 'setMessages' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/AiFieldButton.tsx[24m[0m
|
||||
[0m [2m11:13[22m [33mwarning[39m 'value' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateSlugButton.tsx[24m[0m
|
||||
[0m [2m20:21[22m [33mwarning[39m 'replaceState' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m21:13[22m [33mwarning[39m 'value' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateThumbnailButton.tsx[24m[0m
|
||||
[0m [2m21:13[22m [33mwarning[39m 'value' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/OptimizeButton.tsx[24m[0m
|
||||
[0m [2m5:10[22m [33mwarning[39m 'Button' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/endpoints/chatEndpoint.ts[24m[0m
|
||||
[0m [2m96:13[22m [31merror[39m Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free [2m@typescript-eslint/ban-ts-comment[22m[0m
|
||||
[0m [2m100:13[22m [31merror[39m Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free [2m@typescript-eslint/ban-ts-comment[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/mcpAdapter.ts[24m[0m
|
||||
[0m [2m44:15[22m [33mwarning[39m 'toolSchema' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m53:13[22m [31merror[39m Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free [2m@typescript-eslint/ban-ts-comment[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/memoryDb.ts[24m[0m
|
||||
[0m [2m50:13[22m [31merror[39m Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free [2m@typescript-eslint/ban-ts-comment[22m[0m
|
||||
[0m [2m88:13[22m [31merror[39m Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free [2m@typescript-eslint/ban-ts-comment[22m[0m
|
||||
[0m [2m89:31[22m [33mwarning[39m 'query' is defined but never used. Allowed unused args must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/payloadLocal.ts[24m[0m
|
||||
[0m [2m3:40[22m [33mwarning[39m 'User' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m25:13[22m [31merror[39m Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free [2m@typescript-eslint/ban-ts-comment[22m[0m
|
||||
[0m [2m45:13[22m [31merror[39m Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free [2m@typescript-eslint/ban-ts-comment[22m[0m
|
||||
[0m [2m61:13[22m [31merror[39m Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free [2m@typescript-eslint/ban-ts-comment[22m[0m
|
||||
[0m [2m78:13[22m [31merror[39m Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free [2m@typescript-eslint/ban-ts-comment[22m[0m
|
||||
[0m [2m95:13[22m [31merror[39m Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free [2m@typescript-eslint/ban-ts-comment[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/types.ts[24m[0m
|
||||
[0m [2m1:15[22m [33mwarning[39m 'Plugin' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/ConceptPDF.tsx[24m[0m
|
||||
[0m [2m4:18[22m [33mwarning[39m 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m5:10[22m [33mwarning[39m 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/EstimationPDF.tsx[24m[0m
|
||||
[0m [2m4:18[22m [33mwarning[39m 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m5:10[22m [33mwarning[39m 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m54:11[22m [33mwarning[39m 'getPageNum' is assigned a value but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/InfoPDF.tsx[24m[0m
|
||||
[0m [2m5:13[22m [33mwarning[39m 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m12:5[22m [33mwarning[39m 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SharedUI.tsx[24m[0m
|
||||
[0m [2m528:5[22m [33mwarning[39m 'bankData' is defined but never used. Allowed unused args must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SimpleLayout.tsx[24m[0m
|
||||
[0m [2m4:52[22m [33mwarning[39m 'PDFText' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m5:26[22m [33mwarning[39m 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[4m/Users/marcmintel/Projects/at-mintel/packages/seo-engine/src/report.ts[24m[0m
|
||||
[0m [2m5:3[22m [33mwarning[39m 'TopicCluster' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m6:3[22m [33mwarning[39m 'ContentGap' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m [2m7:3[22m [33mwarning[39m 'CompetitorRanking' is defined but never used. Allowed unused vars must match /^_/u [2m@typescript-eslint/no-unused-vars[22m[0m
|
||||
[0m[0m
|
||||
[0m[31m[1m✖ 49 problems (16 errors, 33 warnings)[22m[39m[0m
|
||||
[0m[31m[1m[22m[39m[31m[1m 1 error and 0 warnings potentially fixable with the `--fix` option.[22m[39m[0m
|
||||
[0m[31m[1m[22m[39m[0m
|
||||
10
package.json
10
package.json
@@ -6,12 +6,12 @@
|
||||
"build": "pnpm -r build",
|
||||
"dev": "pnpm -r dev",
|
||||
"dev:gatekeeper": "bash -c 'trap \"COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml up --build --remove-orphans'",
|
||||
"dev:mcps:up": "docker-compose -f docker-compose.mcps.yml up -d",
|
||||
"dev:mcps:down": "docker-compose -f docker-compose.mcps.yml down && pm2 delete ecosystem.mcps.config.cjs || true",
|
||||
"dev:mcps:up": "docker-compose -f docker-compose.mcps.yml up -d --build --remove-orphans",
|
||||
"dev:mcps:down": "docker-compose -f docker-compose.mcps.yml down",
|
||||
"dev:mcps:watch": "pnpm -r --filter=\"./packages/*-mcp\" exec tsc -w",
|
||||
"dev:mcps": "npm run dev:mcps:up && pm2 start ecosystem.mcps.config.cjs --watch && npm run dev:mcps:watch",
|
||||
"start:mcps:run": "pm2 start ecosystem.mcps.config.cjs",
|
||||
"start:mcps": "npm run dev:mcps:up && npm run start:mcps:run",
|
||||
"dev:mcps": "npm run dev:mcps:up && npm run dev:mcps:watch",
|
||||
"start:mcps": "npm run dev:mcps:up",
|
||||
"start:mcps:force": "docker-compose -f docker-compose.mcps.yml up -d --build --force-recreate --remove-orphans",
|
||||
"lint": "pnpm -r --filter='./packages/**' --filter='./apps/**' lint",
|
||||
"test": "pnpm -r test",
|
||||
"changeset": "changeset",
|
||||
|
||||
@@ -15,5 +15,5 @@ COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Use node to run the compiled index.js
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
# Use node to run the compiled start.js
|
||||
ENTRYPOINT ["node", "dist/start.js"]
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"zod": "^3.23.8"
|
||||
"express": "^5.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,9 @@ import { fileURLToPath } from 'url';
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
// Try to load .env.local first (contains credentials usually)
|
||||
config({ path: resolve(__dirname, '../../../.env.local') });
|
||||
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
|
||||
// Fallback to .env (contains defaults)
|
||||
config({ path: resolve(__dirname, '../../../.env') });
|
||||
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
|
||||
|
||||
// Now boot the compiled MCP index
|
||||
import('./index.js').catch(err => {
|
||||
|
||||
15
packages/glitchtip-mcp/Dockerfile
Normal file
15
packages/glitchtip-mcp/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM node:20-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN corepack enable pnpm && pnpm install --ignore-workspace
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN pnpm build
|
||||
|
||||
FROM node:20-bookworm-slim
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
ENTRYPOINT ["node", "dist/start.js"]
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import express from 'express';
|
||||
import express, { Request, Response } from 'express';
|
||||
import crypto from 'crypto';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
@@ -141,17 +142,34 @@ async function run() {
|
||||
console.error('GlitchTip MCP server is running on stdio');
|
||||
} else {
|
||||
const app = express();
|
||||
let transport: SSEServerTransport | null = null;
|
||||
const transports = new Map<string, SSEServerTransport>();
|
||||
|
||||
app.use((req, _res, next) => {
|
||||
console.error(`${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/sse', async (req, res) => {
|
||||
console.error('New SSE connection established');
|
||||
transport = new SSEServerTransport('/message', res);
|
||||
const sessionId = crypto.randomUUID();
|
||||
console.error(`New SSE connection: ${sessionId}`);
|
||||
const transport = new SSEServerTransport(`/message/${sessionId}`, res);
|
||||
transports.set(sessionId, transport);
|
||||
|
||||
req.on('close', () => {
|
||||
console.error(`SSE connection closed: ${sessionId}`);
|
||||
transports.delete(sessionId);
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
});
|
||||
|
||||
app.post('/message', async (req, res) => {
|
||||
app.post('/message/:sessionId', async (req: Request, res: Response) => {
|
||||
const sessionId = req.params.sessionId;
|
||||
const transport = transports.get(sessionId as string);
|
||||
|
||||
if (!transport) {
|
||||
res.status(400).send('No active SSE connection');
|
||||
console.error(`No transport found for session: ${sessionId}`);
|
||||
res.status(400).send('No active SSE connection for this session');
|
||||
return;
|
||||
}
|
||||
await transport.handlePostMessage(req, res);
|
||||
|
||||
@@ -4,8 +4,8 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
config({ path: resolve(__dirname, '../../../.env.local') });
|
||||
config({ path: resolve(__dirname, '../../../.env') });
|
||||
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
|
||||
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
|
||||
|
||||
import('./index.js').catch(err => {
|
||||
console.error('Failed to start GlitchTip MCP Server:', err);
|
||||
|
||||
15
packages/klz-payload-mcp/Dockerfile
Normal file
15
packages/klz-payload-mcp/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM node:20-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN corepack enable pnpm && pnpm install --ignore-workspace
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN pnpm build
|
||||
|
||||
FROM node:20-bookworm-slim
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
ENTRYPOINT ["node", "dist/start.js"]
|
||||
@@ -13,13 +13,12 @@
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"zod": "^3.23.8"
|
||||
"express": "^5.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^20.14.10",
|
||||
"typescript": "^5.5.3",
|
||||
"tsx": "^4.19.2"
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import axios from "axios";
|
||||
import crypto from "crypto";
|
||||
import https from "https";
|
||||
|
||||
const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://klz-cables.com";
|
||||
@@ -588,17 +589,34 @@ async function run() {
|
||||
console.error('KLZ Payload MCP server is running on stdio');
|
||||
} else {
|
||||
const app = express();
|
||||
let transport: SSEServerTransport | null = null;
|
||||
const transports = new Map<string, SSEServerTransport>();
|
||||
|
||||
app.use((req, _res, next) => {
|
||||
console.error(`${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/sse', async (req: Request, res: Response) => {
|
||||
console.error('New SSE connection established');
|
||||
transport = new SSEServerTransport('/message', res);
|
||||
const sessionId = crypto.randomUUID();
|
||||
console.error(`New SSE connection: ${sessionId}`);
|
||||
const transport = new SSEServerTransport(`/message/${sessionId}`, res);
|
||||
transports.set(sessionId, transport);
|
||||
|
||||
req.on('close', () => {
|
||||
console.error(`SSE connection closed: ${sessionId}`);
|
||||
transports.delete(sessionId);
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
});
|
||||
|
||||
app.post('/message', async (req: Request, res: Response) => {
|
||||
app.post('/message/:sessionId', async (req: Request, res: Response) => {
|
||||
const sessionId = req.params.sessionId;
|
||||
const transport = transports.get(sessionId as string);
|
||||
|
||||
if (!transport) {
|
||||
res.status(400).send('No active SSE connection');
|
||||
console.error(`No transport found for session: ${sessionId}`);
|
||||
res.status(400).send('No active SSE connection for this session');
|
||||
return;
|
||||
}
|
||||
await transport.handlePostMessage(req, res);
|
||||
|
||||
@@ -4,8 +4,8 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
config({ path: resolve(__dirname, '../../../.env.local') });
|
||||
config({ path: resolve(__dirname, '../../../.env') });
|
||||
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
|
||||
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
|
||||
|
||||
import('./index.js').catch(err => {
|
||||
console.error('Failed to start KLZ Payload MCP Server:', err);
|
||||
|
||||
18
packages/memory-mcp/Dockerfile
Normal file
18
packages/memory-mcp/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM node:20-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN corepack enable pnpm && pnpm install --ignore-workspace
|
||||
RUN for dir in $(find /app/node_modules -type d -name "sharp" | grep "node_modules/sharp$"); do \
|
||||
echo "module.exports = {};" > "$dir/lib/index.js" || true; \
|
||||
done
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN pnpm build
|
||||
|
||||
FROM node:20-bookworm-slim
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
ENTRYPOINT ["node", "dist/start.js"]
|
||||
@@ -14,6 +14,7 @@
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"@qdrant/js-client-rest": "^1.12.0",
|
||||
"@xenova/transformers": "^2.17.2",
|
||||
"onnxruntime-node": "^1.14.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"zod": "^3.23.8"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||
import express from 'express';
|
||||
import crypto from 'crypto';
|
||||
import { z } from 'zod';
|
||||
import { QdrantMemoryService } from './qdrant.js';
|
||||
|
||||
@@ -77,17 +78,34 @@ async function main() {
|
||||
}
|
||||
} else {
|
||||
const app = express();
|
||||
let transport: SSEServerTransport | null = null;
|
||||
const transports = new Map<string, SSEServerTransport>();
|
||||
|
||||
app.use((req, _res, next) => {
|
||||
console.error(`${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/sse', async (req, res) => {
|
||||
console.error('New SSE connection established');
|
||||
transport = new SSEServerTransport('/message', res);
|
||||
const sessionId = crypto.randomUUID();
|
||||
console.error(`New SSE connection: ${sessionId}`);
|
||||
const transport = new SSEServerTransport(`/message/${sessionId}`, res);
|
||||
transports.set(sessionId, transport);
|
||||
|
||||
req.on('close', () => {
|
||||
console.error(`SSE connection closed: ${sessionId}`);
|
||||
transports.delete(sessionId);
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
});
|
||||
|
||||
app.post('/message', async (req, res) => {
|
||||
app.post('/message/:sessionId', async (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const transport = transports.get(sessionId as string);
|
||||
|
||||
if (!transport) {
|
||||
res.status(400).send('No active SSE connection');
|
||||
console.error(`No transport found for session: ${sessionId}`);
|
||||
res.status(400).send('No active SSE connection for this session');
|
||||
return;
|
||||
}
|
||||
await transport.handlePostMessage(req, res);
|
||||
|
||||
@@ -5,9 +5,9 @@ import { fileURLToPath } from 'url';
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
// Try to load .env.local first (contains credentials usually)
|
||||
config({ path: resolve(__dirname, '../../../.env.local') });
|
||||
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
|
||||
// Fallback to .env (contains defaults)
|
||||
config({ path: resolve(__dirname, '../../../.env') });
|
||||
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
|
||||
|
||||
// Now boot the compiled MCP index
|
||||
import('./index.js').catch(err => {
|
||||
|
||||
@@ -8,6 +8,9 @@ import path from "node:path";
|
||||
export const baseNextConfig = {
|
||||
output: "standalone",
|
||||
turbopack: {},
|
||||
serverActions: {
|
||||
allowedOrigins: ["*.klz-cables.com", "*.branch.klz-cables.com", "localhost:3000", "*.mintel.me"],
|
||||
},
|
||||
images: {
|
||||
dangerouslyAllowSVG: true,
|
||||
contentDispositionType: "attachment",
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./components/*": "./dist/components/*",
|
||||
"./actions/*": "./dist/actions/*",
|
||||
"./globals/*": "./dist/globals/*",
|
||||
"./endpoints/*": "./dist/endpoints/*",
|
||||
"./utils/*": "./dist/utils/*",
|
||||
"./tools/*": "./dist/tools/*"
|
||||
"./components/FieldGenerators/*": "./dist/components/FieldGenerators/*.js",
|
||||
"./components/*": "./dist/components/*.js",
|
||||
"./actions/*": "./dist/actions/*.js",
|
||||
"./globals/*": "./dist/globals/*.js",
|
||||
"./endpoints/*": "./dist/endpoints/*.js",
|
||||
"./utils/*": "./dist/utils/*.js",
|
||||
"./tools/*": "./dist/tools/*.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@payloadcms/next": ">=3.0.0",
|
||||
|
||||
@@ -1,90 +1,98 @@
|
||||
import type { Config, Plugin } from 'payload'
|
||||
import { AIChatPermissionsCollection } from './collections/AIChatPermissions.js'
|
||||
import type { PayloadChatPluginConfig } from './types.js'
|
||||
import { optimizePostEndpoint } from './endpoints/optimizeEndpoint.js'
|
||||
import { generateSlugEndpoint, generateThumbnailEndpoint, generateSingleFieldEndpoint } from './endpoints/generateEndpoints.js'
|
||||
import type { Plugin } from "payload";
|
||||
import { AIChatPermissionsCollection } from "./collections/AIChatPermissions.js";
|
||||
import type { PayloadChatPluginConfig } from "./types.js";
|
||||
import { optimizePostEndpoint } from "./endpoints/optimizeEndpoint.js";
|
||||
import {
|
||||
generateSlugEndpoint,
|
||||
generateThumbnailEndpoint,
|
||||
generateSingleFieldEndpoint,
|
||||
} from "./endpoints/generateEndpoints.js";
|
||||
|
||||
export const payloadChatPlugin =
|
||||
(pluginOptions: PayloadChatPluginConfig): Plugin =>
|
||||
(incomingConfig) => {
|
||||
let config = { ...incomingConfig }
|
||||
(pluginOptions: PayloadChatPluginConfig): Plugin =>
|
||||
(incomingConfig) => {
|
||||
const config = { ...incomingConfig };
|
||||
|
||||
// If disabled, return config untouched
|
||||
if (pluginOptions.enabled === false) {
|
||||
return config
|
||||
}
|
||||
// If disabled, return config untouched
|
||||
if (pluginOptions.enabled === false) {
|
||||
return config;
|
||||
}
|
||||
|
||||
// 1. Inject the Permissions Collection into the Schema
|
||||
const existingCollections = config.collections || []
|
||||
// 1. Inject the Permissions Collection into the Schema
|
||||
const existingCollections = config.collections || [];
|
||||
|
||||
const mcpServers = pluginOptions.mcpServers || []
|
||||
const mcpServers = pluginOptions.mcpServers || [];
|
||||
|
||||
// Dynamically populate the select options for Collections and MCP Servers
|
||||
const permissionCollection = { ...AIChatPermissionsCollection }
|
||||
const collectionField = permissionCollection.fields.find(f => 'name' in f && f.name === 'allowedCollections') as any
|
||||
if (collectionField) {
|
||||
collectionField.options = existingCollections.map(c => ({
|
||||
label: c.labels?.singular || c.slug,
|
||||
value: c.slug
|
||||
}))
|
||||
}
|
||||
// Dynamically populate the select options for Collections and MCP Servers
|
||||
const permissionCollection = { ...AIChatPermissionsCollection };
|
||||
const collectionField = permissionCollection.fields.find(
|
||||
(f) => "name" in f && f.name === "allowedCollections",
|
||||
) as any;
|
||||
if (collectionField) {
|
||||
collectionField.options = existingCollections.map((c) => ({
|
||||
label: c.labels?.singular || c.slug,
|
||||
value: c.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
const mcpField = permissionCollection.fields.find(f => 'name' in f && f.name === 'allowedMcpServers') as any
|
||||
if (mcpField) {
|
||||
mcpField.options = mcpServers.map(s => ({
|
||||
label: s.name,
|
||||
value: s.name
|
||||
}))
|
||||
}
|
||||
const mcpField = permissionCollection.fields.find(
|
||||
(f) => "name" in f && f.name === "allowedMcpServers",
|
||||
) as any;
|
||||
if (mcpField) {
|
||||
mcpField.options = mcpServers.map((s) => ({
|
||||
label: s.name,
|
||||
value: s.name,
|
||||
}));
|
||||
}
|
||||
|
||||
config.collections = [...existingCollections, permissionCollection]
|
||||
config.collections = [...existingCollections, permissionCollection];
|
||||
|
||||
// 2. Register Custom API Endpoint for the AI Chat
|
||||
config.endpoints = [
|
||||
...(config.endpoints || []),
|
||||
{
|
||||
path: '/api/mcp-chat',
|
||||
method: 'post',
|
||||
handler: async (req) => {
|
||||
// Fallback simple handler while developing endpoint logic
|
||||
return Response.json({ message: "Chat endpoint active" })
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/api/mintel-ai/optimize',
|
||||
method: 'post',
|
||||
handler: optimizePostEndpoint,
|
||||
},
|
||||
{
|
||||
path: '/api/mintel-ai/generate-slug',
|
||||
method: 'post',
|
||||
handler: generateSlugEndpoint,
|
||||
},
|
||||
{
|
||||
path: '/api/mintel-ai/generate-thumbnail',
|
||||
method: 'post',
|
||||
handler: generateThumbnailEndpoint,
|
||||
},
|
||||
{
|
||||
path: '/api/mintel-ai/generate-single-field',
|
||||
method: 'post',
|
||||
handler: generateSingleFieldEndpoint,
|
||||
},
|
||||
]
|
||||
// 2. Register Custom API Endpoint for the AI Chat
|
||||
config.endpoints = [
|
||||
...(config.endpoints || []),
|
||||
{
|
||||
path: "/api/mcp-chat",
|
||||
method: "post",
|
||||
handler: async (_req) => {
|
||||
// Fallback simple handler while developing endpoint logic
|
||||
return Response.json({ message: "Chat endpoint active" });
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/api/mintel-ai/optimize",
|
||||
method: "post",
|
||||
handler: optimizePostEndpoint,
|
||||
},
|
||||
{
|
||||
path: "/api/mintel-ai/generate-slug",
|
||||
method: "post",
|
||||
handler: generateSlugEndpoint,
|
||||
},
|
||||
{
|
||||
path: "/api/mintel-ai/generate-thumbnail",
|
||||
method: "post",
|
||||
handler: generateThumbnailEndpoint,
|
||||
},
|
||||
{
|
||||
path: "/api/mintel-ai/generate-single-field",
|
||||
method: "post",
|
||||
handler: generateSingleFieldEndpoint,
|
||||
},
|
||||
];
|
||||
|
||||
// 3. Inject Chat React Component into Admin UI
|
||||
if (pluginOptions.renderChatBubble !== false) {
|
||||
config.admin = {
|
||||
...(config.admin || {}),
|
||||
components: {
|
||||
...(config.admin?.components || {}),
|
||||
providers: [
|
||||
...(config.admin?.components?.providers || []),
|
||||
'@mintel/payload-ai/components/ChatWindow#ChatWindowProvider',
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
// 3. Inject Chat React Component into Admin UI
|
||||
if (pluginOptions.renderChatBubble !== false) {
|
||||
config.admin = {
|
||||
...(config.admin || {}),
|
||||
components: {
|
||||
...(config.admin?.components || {}),
|
||||
providers: [
|
||||
...(config.admin?.components?.providers || []),
|
||||
"@mintel/payload-ai/components/ChatWindow/index#ChatWindowProvider",
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
@@ -1,136 +1,156 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useChat } from '@ai-sdk/react'
|
||||
import './ChatWindow.scss'
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
|
||||
export const ChatWindowProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<ChatWindow />
|
||||
</>
|
||||
)
|
||||
}
|
||||
export const ChatWindowProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<ChatWindow />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ChatWindow: React.FC = () => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [pageContext, setPageContext] = useState<any>({ url: '' })
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [pageContext, setPageContext] = useState<any>({ url: "" });
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const path = window.location.pathname;
|
||||
let collectionSlug = null;
|
||||
let id = null;
|
||||
// Payload admin URLs are usually /admin/collections/:slug/:id
|
||||
const match = path.match(/\/collections\/([^/]+)(?:\/([^/]+))?/);
|
||||
if (match) {
|
||||
collectionSlug = match[1];
|
||||
if (match[2] && match[2] !== 'create') {
|
||||
id = match[2];
|
||||
}
|
||||
}
|
||||
|
||||
setPageContext({
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
collectionSlug,
|
||||
id
|
||||
})
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const path = window.location.pathname;
|
||||
let collectionSlug = null;
|
||||
let id = null;
|
||||
// Payload admin URLs are usually /admin/collections/:slug/:id
|
||||
const match = path.match(/\/collections\/([^/]+)(?:\/([^/]+))?/);
|
||||
if (match) {
|
||||
collectionSlug = match[1];
|
||||
if (match[2] && match[2] !== "create") {
|
||||
id = match[2];
|
||||
}
|
||||
}, [isOpen]) // Refresh context when chat is opened
|
||||
}
|
||||
|
||||
// @ts-ignore - AI hook version mismatch between core and react packages
|
||||
const { messages, input, handleInputChange, handleSubmit, setMessages } = useChat({
|
||||
api: '/api/mcp-chat',
|
||||
initialMessages: [],
|
||||
body: {
|
||||
pageContext
|
||||
}
|
||||
} as any)
|
||||
setPageContext({
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
collectionSlug,
|
||||
id,
|
||||
});
|
||||
}
|
||||
}, [isOpen]); // Refresh context when chat is opened
|
||||
|
||||
// Basic implementation to toggle chat window and submit messages
|
||||
return (
|
||||
<div className="payload-mcp-chat-container">
|
||||
<button
|
||||
className="payload-mcp-chat-toggle"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
const { messages, input, handleInputChange, handleSubmit } = useChat({
|
||||
api: "/api/mcp-chat",
|
||||
initialMessages: [],
|
||||
body: {
|
||||
pageContext,
|
||||
},
|
||||
} as any) as any;
|
||||
|
||||
// Basic implementation to toggle chat window and submit messages
|
||||
return (
|
||||
<div className="payload-mcp-chat-container">
|
||||
<button
|
||||
className="payload-mcp-chat-toggle"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: "20px",
|
||||
right: "20px",
|
||||
zIndex: 9999,
|
||||
padding: "12px 24px",
|
||||
backgroundColor: "#000",
|
||||
color: "#fff",
|
||||
borderRadius: "8px",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
{isOpen ? "Close AI Chat" : "Ask AI"}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="payload-mcp-chat-window"
|
||||
style={{
|
||||
position: "fixed",
|
||||
bottom: "80px",
|
||||
right: "20px",
|
||||
width: "450px",
|
||||
height: "650px",
|
||||
backgroundColor: "#fff",
|
||||
border: "1px solid #eaeaea",
|
||||
borderRadius: "12px",
|
||||
zIndex: 9999,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
boxShadow: "0 10px 40px rgba(0,0,0,0.1)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="chat-header"
|
||||
style={{
|
||||
padding: "16px",
|
||||
borderBottom: "1px solid #eaeaea",
|
||||
backgroundColor: "#f9f9f9",
|
||||
borderTopLeftRadius: "12px",
|
||||
borderTopRightRadius: "12px",
|
||||
}}
|
||||
>
|
||||
<h3 style={{ margin: 0, fontSize: "16px" }}>Payload MCP Chat</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="chat-messages"
|
||||
style={{ flex: 1, padding: "16px", overflowY: "auto" }}
|
||||
>
|
||||
{messages.map((m: any) => (
|
||||
<div
|
||||
key={m.id}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
right: '20px',
|
||||
zIndex: 9999,
|
||||
padding: '12px 24px',
|
||||
backgroundColor: '#000',
|
||||
color: '#fff',
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
marginBottom: "12px",
|
||||
textAlign: m.role === "user" ? "right" : "left",
|
||||
}}
|
||||
>
|
||||
{isOpen ? 'Close AI Chat' : 'Ask AI'}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
>
|
||||
<div
|
||||
className="payload-mcp-chat-window"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '80px',
|
||||
right: '20px',
|
||||
width: '450px',
|
||||
height: '650px',
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #eaeaea',
|
||||
borderRadius: '12px',
|
||||
zIndex: 9999,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: '0 10px 40px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
style={{
|
||||
display: "inline-block",
|
||||
padding: "8px 12px",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: m.role === "user" ? "#000" : "#f0f0f0",
|
||||
color: m.role === "user" ? "#fff" : "#000",
|
||||
maxWidth: "80%",
|
||||
}}
|
||||
>
|
||||
<div className="chat-header" style={{ padding: '16px', borderBottom: '1px solid #eaeaea', backgroundColor: '#f9f9f9', borderTopLeftRadius: '12px', borderTopRightRadius: '12px' }}>
|
||||
<h3 style={{ margin: 0, fontSize: '16px' }}>Payload MCP Chat</h3>
|
||||
</div>
|
||||
|
||||
<div className="chat-messages" style={{ flex: 1, padding: '16px', overflowY: 'auto' }}>
|
||||
{messages.map((m: any) => (
|
||||
<div key={m.id} style={{
|
||||
marginBottom: '12px',
|
||||
textAlign: m.role === 'user' ? 'right' : 'left'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'inline-block',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: m.role === 'user' ? '#000' : '#f0f0f0',
|
||||
color: m.role === 'user' ? '#fff' : '#000',
|
||||
maxWidth: '80%'
|
||||
}}>
|
||||
{m.role === 'user' ? 'G: ' : 'AI: '}
|
||||
{m.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} style={{ padding: '16px', borderTop: '1px solid #eaeaea' }}>
|
||||
<input
|
||||
value={input}
|
||||
placeholder="Ask me anything or use /commands..."
|
||||
onChange={handleInputChange}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #eaeaea',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
{m.role === "user" ? "G: " : "AI: "}
|
||||
{m.content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{ padding: "16px", borderTop: "1px solid #eaeaea" }}
|
||||
>
|
||||
<input
|
||||
value={input}
|
||||
placeholder="Ask me anything or use /commands..."
|
||||
onChange={handleInputChange}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "12px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid #eaeaea",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,115 +1,142 @@
|
||||
import { streamText } from 'ai'
|
||||
import { createOpenAI } from '@ai-sdk/openai'
|
||||
import { generatePayloadLocalTools } from '../tools/payloadLocal.js'
|
||||
import { createMcpTools } from '../tools/mcpAdapter.js'
|
||||
import { generateMemoryTools } from '../tools/memoryDb.js'
|
||||
import type { PayloadRequest } from 'payload'
|
||||
import { streamText } from "ai";
|
||||
import { createOpenAI } from "@ai-sdk/openai";
|
||||
import { generatePayloadLocalTools } from "../tools/payloadLocal.js";
|
||||
import { createMcpTools } from "../tools/mcpAdapter.js";
|
||||
import { generateMemoryTools } from "../tools/memoryDb.js";
|
||||
import type { PayloadRequest } from "payload";
|
||||
|
||||
const openrouter = createOpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
apiKey: process.env.OPENROUTER_API_KEY || 'dummy_key',
|
||||
})
|
||||
baseURL: "https://openrouter.ai/api/v1",
|
||||
apiKey: process.env.OPENROUTER_API_KEY || "dummy_key",
|
||||
});
|
||||
|
||||
export const handleMcpChat = async (req: PayloadRequest) => {
|
||||
if (!req.user) {
|
||||
return Response.json({ error: 'Unauthorized. You must be logged in to use AI Chat.' }, { status: 401 })
|
||||
if (!req.user) {
|
||||
return Response.json(
|
||||
{ error: "Unauthorized. You must be logged in to use AI Chat." },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const { messages, pageContext } = ((await req.json?.()) || {
|
||||
messages: [],
|
||||
}) as { messages: any[]; pageContext?: any };
|
||||
|
||||
// 1. Check AI Permissions for req.user
|
||||
// Look up the collection for permissions
|
||||
const permissionsQuery = await req.payload.find({
|
||||
collection: "ai-chat-permissions" as any,
|
||||
where: {
|
||||
or: [
|
||||
{ targetUser: { equals: req.user.id } },
|
||||
{ targetRole: { equals: req.user.role || "admin" } },
|
||||
],
|
||||
},
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
const allowedCollections = new Set<string>();
|
||||
const allowedMcpServers = new Set<string>();
|
||||
|
||||
for (const perm of permissionsQuery.docs) {
|
||||
if (perm.allowedCollections) {
|
||||
perm.allowedCollections.forEach((c: string) => allowedCollections.add(c));
|
||||
}
|
||||
|
||||
const { messages, pageContext } = (await req.json?.() || { messages: [] }) as { messages: any[], pageContext?: any }
|
||||
|
||||
// 1. Check AI Permissions for req.user
|
||||
// Look up the collection for permissions
|
||||
const permissionsQuery = await req.payload.find({
|
||||
collection: 'ai-chat-permissions' as any,
|
||||
where: {
|
||||
or: [
|
||||
{ targetUser: { equals: req.user.id } },
|
||||
{ targetRole: { equals: req.user.role || 'admin' } }
|
||||
]
|
||||
},
|
||||
limit: 10
|
||||
})
|
||||
|
||||
const allowedCollections = new Set<string>()
|
||||
const allowedMcpServers = new Set<string>()
|
||||
|
||||
for (const perm of permissionsQuery.docs) {
|
||||
if (perm.allowedCollections) {
|
||||
perm.allowedCollections.forEach((c: string) => allowedCollections.add(c))
|
||||
}
|
||||
if (perm.allowedMcpServers) {
|
||||
perm.allowedMcpServers.forEach((s: string) => allowedMcpServers.add(s))
|
||||
}
|
||||
if (perm.allowedMcpServers) {
|
||||
perm.allowedMcpServers.forEach((s: string) => allowedMcpServers.add(s));
|
||||
}
|
||||
}
|
||||
|
||||
let accessCollections = Array.from(allowedCollections)
|
||||
if (accessCollections.length === 0) {
|
||||
// Fallback or demo config if not configured yet
|
||||
accessCollections = ['users', 'pages', 'posts', 'products', 'leads', 'media']
|
||||
let accessCollections = Array.from(allowedCollections);
|
||||
if (accessCollections.length === 0) {
|
||||
// Fallback or demo config if not configured yet
|
||||
accessCollections = [
|
||||
"users",
|
||||
"pages",
|
||||
"posts",
|
||||
"products",
|
||||
"leads",
|
||||
"media",
|
||||
];
|
||||
}
|
||||
|
||||
let activeTools: Record<string, any> = {};
|
||||
|
||||
// 2. Generate Payload Local Tools
|
||||
if (accessCollections.length > 0) {
|
||||
const payloadTools = generatePayloadLocalTools(
|
||||
req.payload,
|
||||
req,
|
||||
accessCollections,
|
||||
);
|
||||
activeTools = { ...activeTools, ...payloadTools };
|
||||
}
|
||||
|
||||
// 3. Connect External MCPs
|
||||
if (Array.from(allowedMcpServers).includes("gitea")) {
|
||||
try {
|
||||
const { tools: giteaTools } = await createMcpTools({
|
||||
name: "gitea",
|
||||
command: "npx",
|
||||
args: [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-gitea",
|
||||
"--url",
|
||||
"https://git.mintel.int",
|
||||
"--token",
|
||||
process.env.GITEA_TOKEN || "",
|
||||
],
|
||||
});
|
||||
activeTools = { ...activeTools, ...giteaTools };
|
||||
} catch (e) {
|
||||
console.error("Failed to connect to Gitea MCP", e);
|
||||
}
|
||||
}
|
||||
|
||||
let activeTools: Record<string, any> = {}
|
||||
// 4. Inject Memory Database Tools
|
||||
// We provide the user ID so memory is partitioned per user
|
||||
const memoryTools = generateMemoryTools(req.user.id);
|
||||
activeTools = { ...activeTools, ...memoryTools };
|
||||
|
||||
// 2. Generate Payload Local Tools
|
||||
if (accessCollections.length > 0) {
|
||||
const payloadTools = generatePayloadLocalTools(req.payload, req, accessCollections)
|
||||
activeTools = { ...activeTools, ...payloadTools }
|
||||
}
|
||||
|
||||
// 3. Connect External MCPs
|
||||
if (Array.from(allowedMcpServers).includes('gitea')) {
|
||||
try {
|
||||
const { tools: giteaTools } = await createMcpTools({
|
||||
name: 'gitea',
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-gitea', '--url', 'https://git.mintel.int', '--token', process.env.GITEA_TOKEN || '']
|
||||
})
|
||||
activeTools = { ...activeTools, ...giteaTools }
|
||||
} catch (e) {
|
||||
console.error('Failed to connect to Gitea MCP', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Inject Memory Database Tools
|
||||
// We provide the user ID so memory is partitioned per user
|
||||
const memoryTools = generateMemoryTools(req.user.id)
|
||||
activeTools = { ...activeTools, ...memoryTools }
|
||||
|
||||
// 5. Build prompt to ensure it asks before saving
|
||||
const memorySystemPrompt = `
|
||||
// 5. Build prompt to ensure it asks before saving
|
||||
const memorySystemPrompt = `
|
||||
You have access to a long-term vector memory database (Qdrant).
|
||||
If the user says "speicher das", "merk dir das", "vergiss das nicht" etc., you MUST use the save_memory tool.
|
||||
If the user shares important context but doesn't explicitly ask you to remember it, you should ask "Soll ich mir das für die Zukunft merken?" before saving it. Do not ask for trivial things.
|
||||
`
|
||||
`;
|
||||
|
||||
const contextContextStr = pageContext ? `
|
||||
const contextContextStr = pageContext
|
||||
? `
|
||||
Current User Context:
|
||||
URL: ${pageContext.url || 'Unknown'}
|
||||
Title: ${pageContext.title || 'Unknown'}
|
||||
Collection: ${pageContext.collectionSlug || 'None'}
|
||||
Document ID: ${pageContext.id || 'None'}
|
||||
URL: ${pageContext.url || "Unknown"}
|
||||
Title: ${pageContext.title || "Unknown"}
|
||||
Collection: ${pageContext.collectionSlug || "None"}
|
||||
Document ID: ${pageContext.id || "None"}
|
||||
You can use this to understand what the user is currently looking at.
|
||||
` : ''
|
||||
`
|
||||
: "";
|
||||
|
||||
try {
|
||||
const result = streamText({
|
||||
// @ts-ignore - AI SDK type mismatch
|
||||
model: openrouter('google/gemini-3.0-flash'),
|
||||
messages,
|
||||
tools: activeTools,
|
||||
// @ts-ignore - AI SDK type mismatch with maxSteps
|
||||
maxSteps: 10,
|
||||
system: `You are a helpful Payload CMS Agent orchestrating the local Mintel ecosystem.
|
||||
try {
|
||||
const result = streamText({
|
||||
model: openrouter("google/gemini-3.0-flash"),
|
||||
messages,
|
||||
tools: activeTools,
|
||||
// @ts-expect-error - AI SDK type mismatch with maxSteps
|
||||
maxSteps: 10,
|
||||
system: `You are a helpful Payload CMS Agent orchestrating the local Mintel ecosystem.
|
||||
You only have access to tools explicitly granted by the Admin.
|
||||
You can completely control Payload CMS (read, create, update, delete documents).
|
||||
If you need more details to fulfill a request (e.g. creating a blog post), you can ask the user.
|
||||
${contextContextStr}
|
||||
${memorySystemPrompt}`
|
||||
})
|
||||
${memorySystemPrompt}`,
|
||||
});
|
||||
|
||||
return result.toTextStreamResponse()
|
||||
} catch (error) {
|
||||
console.error("AI Error:", error)
|
||||
return Response.json({ error: 'Failed to process AI request' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
return result.toTextStreamResponse();
|
||||
} catch (error) {
|
||||
console.error("AI Error:", error);
|
||||
return Response.json(
|
||||
{ error: "Failed to process AI request" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,126 +4,157 @@ import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
|
||||
async function getOrchestrator() {
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||
|
||||
if (!OPENROUTER_KEY) {
|
||||
throw new Error(
|
||||
"Missing OPENROUTER_API_KEY in .env (Required for AI generation)",
|
||||
);
|
||||
}
|
||||
|
||||
const importDynamic = new Function("modulePath", "return import(modulePath)");
|
||||
const { AiBlogPostOrchestrator } = await importDynamic(
|
||||
"@mintel/content-engine",
|
||||
if (!OPENROUTER_KEY) {
|
||||
throw new Error(
|
||||
"Missing OPENROUTER_API_KEY in .env (Required for AI generation)",
|
||||
);
|
||||
}
|
||||
|
||||
return new AiBlogPostOrchestrator({
|
||||
apiKey: OPENROUTER_KEY,
|
||||
replicateApiKey: REPLICATE_KEY,
|
||||
model: "google/gemini-3-flash-preview",
|
||||
});
|
||||
const importDynamic = new Function("modulePath", "return import(modulePath)");
|
||||
const { AiBlogPostOrchestrator } = await importDynamic(
|
||||
"@mintel/content-engine",
|
||||
);
|
||||
|
||||
return new AiBlogPostOrchestrator({
|
||||
apiKey: OPENROUTER_KEY,
|
||||
replicateApiKey: REPLICATE_KEY,
|
||||
model: "google/gemini-3-flash-preview",
|
||||
});
|
||||
}
|
||||
|
||||
export const generateSlugEndpoint = async (req: PayloadRequest) => {
|
||||
try {
|
||||
let body: any = {};
|
||||
try {
|
||||
const { title, draftContent, oldSlug, instructions } = (await req.json?.() || {}) as any;
|
||||
const orchestrator = await getOrchestrator();
|
||||
const newSlug = await orchestrator.generateSlug(
|
||||
draftContent,
|
||||
title,
|
||||
instructions,
|
||||
);
|
||||
|
||||
if (oldSlug && oldSlug !== newSlug) {
|
||||
await req.payload.create({
|
||||
collection: "redirects" as any,
|
||||
data: {
|
||||
from: oldSlug,
|
||||
to: newSlug,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json({ success: true, slug: newSlug });
|
||||
} catch (e: any) {
|
||||
return Response.json({ success: false, error: e.message }, { status: 500 });
|
||||
if (req.body) body = (await req.json?.()) || {};
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
const { title, draftContent, oldSlug, instructions } = body;
|
||||
const orchestrator = await getOrchestrator();
|
||||
const newSlug = await orchestrator.generateSlug(
|
||||
draftContent,
|
||||
title,
|
||||
instructions,
|
||||
);
|
||||
|
||||
if (oldSlug && oldSlug !== newSlug) {
|
||||
await req.payload.create({
|
||||
collection: "redirects" as any,
|
||||
data: {
|
||||
from: oldSlug,
|
||||
to: newSlug,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json({ success: true, slug: newSlug });
|
||||
} catch (e: any) {
|
||||
return Response.json({ success: false, error: e.message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const generateThumbnailEndpoint = async (req: PayloadRequest) => {
|
||||
try {
|
||||
let body: any = {};
|
||||
try {
|
||||
const { draftContent, title, instructions } = (await req.json?.() || {}) as any;
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||
|
||||
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY in .env");
|
||||
if (!REPLICATE_KEY) throw new Error("Missing REPLICATE_API_KEY in .env");
|
||||
|
||||
const importDynamic = new Function("modulePath", "return import(modulePath)");
|
||||
const { AiBlogPostOrchestrator } = await importDynamic("@mintel/content-engine");
|
||||
const { ThumbnailGenerator } = await importDynamic("@mintel/thumbnail-generator");
|
||||
|
||||
const orchestrator = new AiBlogPostOrchestrator({
|
||||
apiKey: OPENROUTER_KEY,
|
||||
replicateApiKey: REPLICATE_KEY,
|
||||
model: "google/gemini-3-flash-preview",
|
||||
});
|
||||
|
||||
const tg = new ThumbnailGenerator({ replicateApiKey: REPLICATE_KEY });
|
||||
|
||||
const prompt = await orchestrator.generateVisualPrompt(
|
||||
draftContent || title || "Technology",
|
||||
instructions,
|
||||
);
|
||||
|
||||
const tmpPath = path.join(os.tmpdir(), `mintel-thumb-${Date.now()}.png`);
|
||||
await tg.generateImage(prompt, tmpPath);
|
||||
|
||||
const fileData = await fs.readFile(tmpPath);
|
||||
const stat = await fs.stat(tmpPath);
|
||||
const fileName = path.basename(tmpPath);
|
||||
|
||||
const newMedia = await req.payload.create({
|
||||
collection: "media" as any,
|
||||
data: {
|
||||
alt: title ? `Thumbnail for ${title}` : "AI Generated Thumbnail",
|
||||
},
|
||||
file: {
|
||||
data: fileData,
|
||||
name: fileName,
|
||||
mimetype: "image/png",
|
||||
size: stat.size,
|
||||
},
|
||||
});
|
||||
|
||||
await fs.unlink(tmpPath).catch(() => { });
|
||||
|
||||
return Response.json({ success: true, mediaId: newMedia.id });
|
||||
} catch (e: any) {
|
||||
return Response.json({ success: false, error: e.message }, { status: 500 });
|
||||
if (req.body) body = (await req.json?.()) || {};
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
const { draftContent, title, instructions } = body;
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||
|
||||
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY in .env");
|
||||
if (!REPLICATE_KEY) throw new Error("Missing REPLICATE_API_KEY in .env");
|
||||
|
||||
const importDynamic = new Function(
|
||||
"modulePath",
|
||||
"return import(modulePath)",
|
||||
);
|
||||
const { AiBlogPostOrchestrator } = await importDynamic(
|
||||
"@mintel/content-engine",
|
||||
);
|
||||
const { ThumbnailGenerator } = await importDynamic(
|
||||
"@mintel/thumbnail-generator",
|
||||
);
|
||||
|
||||
const orchestrator = new AiBlogPostOrchestrator({
|
||||
apiKey: OPENROUTER_KEY,
|
||||
replicateApiKey: REPLICATE_KEY,
|
||||
model: "google/gemini-3-flash-preview",
|
||||
});
|
||||
|
||||
const tg = new ThumbnailGenerator({ replicateApiKey: REPLICATE_KEY });
|
||||
|
||||
const prompt = await orchestrator.generateVisualPrompt(
|
||||
draftContent || title || "Technology",
|
||||
instructions,
|
||||
);
|
||||
|
||||
const tmpPath = path.join(os.tmpdir(), `mintel-thumb-${Date.now()}.png`);
|
||||
await tg.generateImage(prompt, tmpPath);
|
||||
|
||||
const fileData = await fs.readFile(tmpPath);
|
||||
const stat = await fs.stat(tmpPath);
|
||||
const fileName = path.basename(tmpPath);
|
||||
|
||||
const newMedia = await req.payload.create({
|
||||
collection: "media" as any,
|
||||
data: {
|
||||
alt: title ? `Thumbnail for ${title}` : "AI Generated Thumbnail",
|
||||
},
|
||||
file: {
|
||||
data: fileData,
|
||||
name: fileName,
|
||||
mimetype: "image/png",
|
||||
size: stat.size,
|
||||
},
|
||||
});
|
||||
|
||||
await fs.unlink(tmpPath).catch(() => {});
|
||||
|
||||
return Response.json({ success: true, mediaId: newMedia.id });
|
||||
} catch (e: any) {
|
||||
return Response.json({ success: false, error: e.message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const generateSingleFieldEndpoint = async (req: PayloadRequest) => {
|
||||
try {
|
||||
let body: any = {};
|
||||
try {
|
||||
const { documentTitle, documentContent, fieldName, fieldDescription, instructions } = (await req.json?.() || {}) as any;
|
||||
if (req.body) body = (await req.json?.()) || {};
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const {
|
||||
documentTitle,
|
||||
documentContent,
|
||||
fieldName,
|
||||
fieldDescription,
|
||||
instructions,
|
||||
} = body;
|
||||
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY");
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY");
|
||||
|
||||
const contextDocsData = await req.payload.find({
|
||||
collection: "context-files" as any,
|
||||
limit: 100,
|
||||
});
|
||||
const projectContext = contextDocsData.docs
|
||||
.map((doc: any) => `--- ${doc.filename} ---\n${doc.content}`)
|
||||
.join("\n\n");
|
||||
const contextDocsData = await req.payload.find({
|
||||
collection: "context-files" as any,
|
||||
limit: 100,
|
||||
});
|
||||
const projectContext = contextDocsData.docs
|
||||
.map((doc: any) => `--- ${doc.filename} ---\n${doc.content}`)
|
||||
.join("\n\n");
|
||||
|
||||
const prompt = `You are an expert AI assistant perfectly trained for generating exact data values for CMS components.
|
||||
const prompt = `You are an expert AI assistant perfectly trained for generating exact data values for CMS components.
|
||||
PROJECT STRATEGY & CONTEXT:
|
||||
${projectContext}
|
||||
|
||||
@@ -138,21 +169,21 @@ CRITICAL RULES:
|
||||
3. If the field implies a diagram or flow, output RAW Mermaid.js code.
|
||||
4. If it's standard text, write professional B2B German. No quotes, no conversational filler.`;
|
||||
|
||||
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${OPENROUTER_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "google/gemini-3-flash-preview",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
const text = data.choices?.[0]?.message?.content?.trim() || "";
|
||||
return Response.json({ success: true, text });
|
||||
} catch (e: any) {
|
||||
return Response.json({ success: false, error: e.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${OPENROUTER_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "google/gemini-3-flash-preview",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
const text = data.choices?.[0]?.message?.content?.trim() || "";
|
||||
return Response.json({ success: true, text });
|
||||
} catch (e: any) {
|
||||
return Response.json({ success: false, error: e.message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,75 +1,108 @@
|
||||
import { PayloadRequest } from 'payload'
|
||||
import { PayloadRequest } from "payload";
|
||||
import { parseMarkdownToLexical } from "../utils/lexicalParser.js";
|
||||
|
||||
export const optimizePostEndpoint = async (req: PayloadRequest) => {
|
||||
try {
|
||||
let body: any = {};
|
||||
try {
|
||||
const { draftContent, instructions } = (await req.json?.() || {}) as { draftContent: string; instructions?: string };
|
||||
|
||||
if (!draftContent) {
|
||||
return Response.json({ error: 'Missing draftContent' }, { status: 400 })
|
||||
}
|
||||
|
||||
const globalAiSettings = (await req.payload.findGlobal({ slug: "ai-settings" })) as any;
|
||||
const customSources =
|
||||
globalAiSettings?.customSources?.map((s: any) => s.sourceName) || [];
|
||||
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||
|
||||
if (!OPENROUTER_KEY) {
|
||||
return Response.json({ error: "OPENROUTER_KEY not found in environment." }, { status: 500 })
|
||||
}
|
||||
|
||||
// Dynamically import to avoid bundling it into client components that might accidentally import this file
|
||||
const importDynamic = new Function("modulePath", "return import(modulePath)");
|
||||
const { AiBlogPostOrchestrator } = await importDynamic("@mintel/content-engine");
|
||||
|
||||
const orchestrator = new AiBlogPostOrchestrator({
|
||||
apiKey: OPENROUTER_KEY,
|
||||
replicateApiKey: REPLICATE_KEY,
|
||||
model: "google/gemini-3-flash-preview",
|
||||
});
|
||||
|
||||
const contextDocsData = await req.payload.find({
|
||||
collection: "context-files" as any,
|
||||
limit: 100,
|
||||
});
|
||||
const projectContext = contextDocsData.docs.map((doc: any) => doc.content);
|
||||
|
||||
const optimizedMarkdown = await orchestrator.optimizeDocument({
|
||||
content: draftContent,
|
||||
projectContext,
|
||||
availableComponents: [],
|
||||
instructions,
|
||||
internalLinks: [],
|
||||
customSources,
|
||||
});
|
||||
|
||||
if (!optimizedMarkdown || typeof optimizedMarkdown !== "string") {
|
||||
return Response.json({ error: "AI returned invalid markup." }, { status: 500 })
|
||||
}
|
||||
|
||||
const blocks = parseMarkdownToLexical(optimizedMarkdown);
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
lexicalAST: {
|
||||
root: {
|
||||
type: "root",
|
||||
format: "",
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: blocks,
|
||||
direction: "ltr",
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error("Failed to optimize post in endpoint:", error);
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: error.message || "An unknown error occurred during optimization.",
|
||||
}, { status: 500 })
|
||||
if (req.body) {
|
||||
// req.json() acts as a method in Next.js/Payload req wrapper
|
||||
body = (await req.json?.()) || {};
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore JSON parse error, body remains empty
|
||||
}
|
||||
}
|
||||
|
||||
const { draftContent, instructions } = body as {
|
||||
draftContent?: string;
|
||||
instructions?: string;
|
||||
};
|
||||
|
||||
if (!draftContent) {
|
||||
return Response.json(
|
||||
{ success: false, error: "Missing draftContent" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const globalAiSettings = (await req.payload.findGlobal({
|
||||
slug: "ai-settings",
|
||||
})) as any;
|
||||
const customSources =
|
||||
globalAiSettings?.customSources?.map((s: any) => s.sourceName) || [];
|
||||
|
||||
const OPENROUTER_KEY =
|
||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||
|
||||
if (!OPENROUTER_KEY) {
|
||||
return Response.json(
|
||||
{ error: "OPENROUTER_KEY not found in environment." },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
// Dynamically import to avoid bundling it into client components that might accidentally import this file
|
||||
const importDynamic = new Function(
|
||||
"modulePath",
|
||||
"return import(modulePath)",
|
||||
);
|
||||
const { AiBlogPostOrchestrator } = await importDynamic(
|
||||
"@mintel/content-engine",
|
||||
);
|
||||
|
||||
const orchestrator = new AiBlogPostOrchestrator({
|
||||
apiKey: OPENROUTER_KEY,
|
||||
replicateApiKey: REPLICATE_KEY,
|
||||
model: "google/gemini-3-flash-preview",
|
||||
});
|
||||
|
||||
const contextDocsData = await req.payload.find({
|
||||
collection: "context-files" as any,
|
||||
limit: 100,
|
||||
});
|
||||
const projectContext = contextDocsData.docs.map((doc: any) => doc.content);
|
||||
|
||||
const optimizedMarkdown = await orchestrator.optimizeDocument({
|
||||
content: draftContent,
|
||||
projectContext,
|
||||
availableComponents: [],
|
||||
instructions,
|
||||
internalLinks: [],
|
||||
customSources,
|
||||
});
|
||||
|
||||
if (!optimizedMarkdown || typeof optimizedMarkdown !== "string") {
|
||||
return Response.json(
|
||||
{ error: "AI returned invalid markup." },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
const blocks = parseMarkdownToLexical(optimizedMarkdown);
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
lexicalAST: {
|
||||
root: {
|
||||
type: "root",
|
||||
format: "",
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: blocks,
|
||||
direction: "ltr",
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Failed to optimize post in endpoint:", error);
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
error.message || "An unknown error occurred during optimization.",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,65 +1,72 @@
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { tool } from 'ai'
|
||||
import { z } from 'zod'
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Connects to an external MCP Server and maps its tools to Vercel AI SDK Tools.
|
||||
*/
|
||||
export async function createMcpTools(mcpConfig: { name: string, url?: string, command?: string, args?: string[] }) {
|
||||
let transport
|
||||
export async function createMcpTools(mcpConfig: {
|
||||
name: string;
|
||||
url?: string;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
}) {
|
||||
let transport;
|
||||
|
||||
// Support both HTTP/SSE and STDIO transports
|
||||
if (mcpConfig.url) {
|
||||
transport = new SSEClientTransport(new URL(mcpConfig.url))
|
||||
} else if (mcpConfig.command) {
|
||||
transport = new StdioClientTransport({
|
||||
command: mcpConfig.command,
|
||||
args: mcpConfig.args || [],
|
||||
})
|
||||
} else {
|
||||
throw new Error('Invalid MCP config: Must provide either URL or Command.')
|
||||
}
|
||||
// Support both HTTP/SSE and STDIO transports
|
||||
if (mcpConfig.url) {
|
||||
transport = new SSEClientTransport(new URL(mcpConfig.url));
|
||||
} else if (mcpConfig.command) {
|
||||
transport = new StdioClientTransport({
|
||||
command: mcpConfig.command,
|
||||
args: mcpConfig.args || [],
|
||||
});
|
||||
} else {
|
||||
throw new Error("Invalid MCP config: Must provide either URL or Command.");
|
||||
}
|
||||
|
||||
const client = new Client(
|
||||
{ name: `payload-ai-client-${mcpConfig.name}`, version: '1.0.0' },
|
||||
{ capabilities: {} }
|
||||
)
|
||||
const client = new Client(
|
||||
{ name: `payload-ai-client-${mcpConfig.name}`, version: "1.0.0" },
|
||||
{ capabilities: {} },
|
||||
);
|
||||
|
||||
await client.connect(transport)
|
||||
await client.connect(transport);
|
||||
|
||||
// Fetch available tools from the external MCP server
|
||||
const toolListResult = await client.listTools()
|
||||
const externalTools = toolListResult.tools || []
|
||||
// Fetch available tools from the external MCP server
|
||||
const toolListResult = await client.listTools();
|
||||
const externalTools = toolListResult.tools || [];
|
||||
|
||||
const aiSdkTools: Record<string, any> = {}
|
||||
const aiSdkTools: Record<string, any> = {};
|
||||
|
||||
// Map each external tool to a Vercel AI SDK Tool
|
||||
for (const extTool of externalTools) {
|
||||
// Basic conversion of JSON Schema to Zod for the AI SDK
|
||||
// Note: For a production ready adapter, you might need a more robust jsonSchemaToZod converter
|
||||
// or use AI SDK's new experimental generateSchema feature if available.
|
||||
// Here we use a generic `z.any()` as a fallback since AI SDK requires a Zod schema.
|
||||
const toolSchema = extTool.inputSchema as Record<string, any>
|
||||
// Map each external tool to a Vercel AI SDK Tool
|
||||
for (const extTool of externalTools) {
|
||||
// Basic conversion of JSON Schema to Zod for the AI SDK
|
||||
// Note: For a production ready adapter, you might need a more robust jsonSchemaToZod converter
|
||||
// or use AI SDK's new experimental generateSchema feature if available.
|
||||
// Here we use a generic `z.any()` as a fallback since AI SDK requires a Zod schema.
|
||||
const toolSchema = extTool.inputSchema as Record<string, any>;
|
||||
|
||||
// We create a simplified parameter parser.
|
||||
// An ideal approach uses `jsonSchemaToZod` library or native AI SDK JSON schema support
|
||||
// (introduced recently in `ai` package).
|
||||
// We create a simplified parameter parser.
|
||||
// An ideal approach uses `jsonSchemaToZod` library or native AI SDK JSON schema support
|
||||
// (introduced recently in `ai` package).
|
||||
|
||||
aiSdkTools[`${mcpConfig.name}_${extTool.name}`] = tool({
|
||||
description: `[From ${mcpConfig.name}] ${extTool.description || extTool.name}`,
|
||||
parameters: z.any().describe('JSON matching the original MCP input_schema'), // Simplify for prototype
|
||||
// @ts-ignore - AI strict mode overload bug with implicit zod inferences
|
||||
execute: async (args: any) => {
|
||||
const result = await client.callTool({
|
||||
name: extTool.name,
|
||||
arguments: args
|
||||
})
|
||||
return result
|
||||
}
|
||||
})
|
||||
}
|
||||
aiSdkTools[`${mcpConfig.name}_${extTool.name}`] = tool({
|
||||
description: `[From ${mcpConfig.name}] ${extTool.description || extTool.name}`,
|
||||
parameters: z
|
||||
.any()
|
||||
.describe("JSON matching the original MCP input_schema"), // Simplify for prototype
|
||||
// @ts-expect-error - AI strict mode overload bug with implicit zod inferences
|
||||
execute: async (args: any) => {
|
||||
const result = await client.callTool({
|
||||
name: extTool.name,
|
||||
arguments: args,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { tools: aiSdkTools, client }
|
||||
return { tools: aiSdkTools, client };
|
||||
}
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
import { tool } from 'ai'
|
||||
import { z } from 'zod'
|
||||
import { QdrantClient } from '@qdrant/js-client-rest'
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
import { QdrantClient } from "@qdrant/js-client-rest";
|
||||
|
||||
// Qdrant initialization
|
||||
// This requires the user to have Qdrant running and QDRANT_URL/QDRANT_API_KEY environment variables set
|
||||
const qdrantClient = new QdrantClient({
|
||||
url: process.env.QDRANT_URL || 'http://localhost:6333',
|
||||
apiKey: process.env.QDRANT_API_KEY,
|
||||
})
|
||||
url: process.env.QDRANT_URL || "http://localhost:6333",
|
||||
apiKey: process.env.QDRANT_API_KEY,
|
||||
});
|
||||
|
||||
const MEMORY_COLLECTION = 'mintel_ai_memory'
|
||||
const MEMORY_COLLECTION = "mintel_ai_memory";
|
||||
|
||||
// Ensure collection exists on load
|
||||
async function initQdrant() {
|
||||
try {
|
||||
const res = await qdrantClient.getCollections()
|
||||
const exists = res.collections.find((c: any) => c.name === MEMORY_COLLECTION)
|
||||
if (!exists) {
|
||||
await qdrantClient.createCollection(MEMORY_COLLECTION, {
|
||||
vectors: {
|
||||
size: 1536, // typical embedding size, adjust based on the embedding model used
|
||||
distance: 'Cosine',
|
||||
},
|
||||
})
|
||||
console.log(`Qdrant collection '${MEMORY_COLLECTION}' created.`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Qdrant memory collection:', error)
|
||||
try {
|
||||
const res = await qdrantClient.getCollections();
|
||||
const exists = res.collections.find(
|
||||
(c: any) => c.name === MEMORY_COLLECTION,
|
||||
);
|
||||
if (!exists) {
|
||||
await qdrantClient.createCollection(MEMORY_COLLECTION, {
|
||||
vectors: {
|
||||
size: 1536, // typical embedding size, adjust based on the embedding model used
|
||||
distance: "Cosine",
|
||||
},
|
||||
});
|
||||
console.log(`Qdrant collection '${MEMORY_COLLECTION}' created.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize Qdrant memory collection:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Call init, but don't block
|
||||
initQdrant()
|
||||
initQdrant();
|
||||
|
||||
/**
|
||||
* Returns memory tools for the AI SDK.
|
||||
@@ -40,76 +42,99 @@ initQdrant()
|
||||
* by a utility function, or we use Qdrant's FastEmbed (if running their specialized container).
|
||||
*/
|
||||
export const generateMemoryTools = (userId: string | number) => {
|
||||
return {
|
||||
save_memory: tool({
|
||||
description: 'Save an important preference, fact, or instruction about the user to long-term memory. Only use this when explicitly asked or when it is clearly a long-term preference.',
|
||||
parameters: z.object({
|
||||
fact: z.string().describe('The fact or instruction to remember.'),
|
||||
category: z.string().optional().describe('An optional category like "preference", "rule", or "project_detail".'),
|
||||
}),
|
||||
// @ts-ignore - AI SDK strict mode bug
|
||||
execute: async ({ fact, category }: { fact: string; category?: string }) => {
|
||||
// In a real scenario, you MUST generate embeddings for the 'fact' string here
|
||||
// using OpenAI or another embedding provider before inserting into Qdrant.
|
||||
// const embedding = await generateEmbedding(fact)
|
||||
return {
|
||||
save_memory: tool({
|
||||
description:
|
||||
"Save an important preference, fact, or instruction about the user to long-term memory. Only use this when explicitly asked or when it is clearly a long-term preference.",
|
||||
parameters: z.object({
|
||||
fact: z.string().describe("The fact or instruction to remember."),
|
||||
category: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'An optional category like "preference", "rule", or "project_detail".',
|
||||
),
|
||||
}),
|
||||
// @ts-expect-error - AI SDK strict mode bug
|
||||
execute: async ({
|
||||
fact,
|
||||
category,
|
||||
}: {
|
||||
fact: string;
|
||||
category?: string;
|
||||
}) => {
|
||||
// In a real scenario, you MUST generate embeddings for the 'fact' string here
|
||||
// using OpenAI or another embedding provider before inserting into Qdrant.
|
||||
// const embedding = await generateEmbedding(fact)
|
||||
|
||||
try {
|
||||
// Mock embedding payload for demonstration
|
||||
const mockEmbedding = new Array(1536).fill(0).map(() => Math.random())
|
||||
try {
|
||||
// Mock embedding payload for demonstration
|
||||
const mockEmbedding = new Array(1536)
|
||||
.fill(0)
|
||||
.map(() => Math.random());
|
||||
|
||||
await qdrantClient.upsert(MEMORY_COLLECTION, {
|
||||
wait: true,
|
||||
points: [
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
vector: mockEmbedding,
|
||||
payload: {
|
||||
userId: String(userId), // Partition memory by user
|
||||
fact,
|
||||
category,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
return { success: true, message: `Successfully remembered: "${fact}"` }
|
||||
} catch (error) {
|
||||
console.error("Qdrant save error:", error)
|
||||
return { success: false, error: 'Failed to save to memory database.' }
|
||||
}
|
||||
await qdrantClient.upsert(MEMORY_COLLECTION, {
|
||||
wait: true,
|
||||
points: [
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
vector: mockEmbedding,
|
||||
payload: {
|
||||
userId: String(userId), // Partition memory by user
|
||||
fact,
|
||||
category,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully remembered: "${fact}"`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Qdrant save error:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to save to memory database.",
|
||||
};
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
search_memory: tool({
|
||||
description:
|
||||
"Search the user's long-term memory for past factual context, preferences, or rules.",
|
||||
parameters: z.object({
|
||||
query: z.string().describe("The search string to find in memory."),
|
||||
}),
|
||||
// @ts-expect-error - AI SDK strict mode bug
|
||||
execute: async ({ query }: { query: string }) => {
|
||||
// Generate embedding for query
|
||||
const mockQueryEmbedding = new Array(1536)
|
||||
.fill(0)
|
||||
.map(() => Math.random());
|
||||
|
||||
try {
|
||||
const results = await qdrantClient.search(MEMORY_COLLECTION, {
|
||||
vector: mockQueryEmbedding,
|
||||
limit: 5,
|
||||
filter: {
|
||||
must: [
|
||||
{
|
||||
key: "userId",
|
||||
match: { value: String(userId) },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
search_memory: tool({
|
||||
description: 'Search the user\'s long-term memory for past factual context, preferences, or rules.',
|
||||
parameters: z.object({
|
||||
query: z.string().describe('The search string to find in memory.'),
|
||||
}),
|
||||
// @ts-ignore - AI SDK strict mode bug
|
||||
execute: async ({ query }: { query: string }) => {
|
||||
// Generate embedding for query
|
||||
const mockQueryEmbedding = new Array(1536).fill(0).map(() => Math.random())
|
||||
|
||||
try {
|
||||
const results = await qdrantClient.search(MEMORY_COLLECTION, {
|
||||
vector: mockQueryEmbedding,
|
||||
limit: 5,
|
||||
filter: {
|
||||
must: [
|
||||
{
|
||||
key: 'userId',
|
||||
match: { value: String(userId) }
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
return results.map((r: any) => r.payload?.fact || '')
|
||||
} catch (error) {
|
||||
console.error("Qdrant search error:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return results.map((r: any) => r.payload?.fact || "");
|
||||
} catch (error) {
|
||||
console.error("Qdrant search error:", error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,107 +1,137 @@
|
||||
import { tool } from 'ai'
|
||||
import { z } from 'zod'
|
||||
import type { Payload, PayloadRequest, User } from 'payload'
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
import type { Payload, PayloadRequest, User } from "payload";
|
||||
|
||||
export const generatePayloadLocalTools = (
|
||||
payload: Payload,
|
||||
req: PayloadRequest,
|
||||
allowedCollections: string[]
|
||||
payload: Payload,
|
||||
req: PayloadRequest,
|
||||
allowedCollections: string[],
|
||||
) => {
|
||||
const tools: Record<string, any> = {}
|
||||
const tools: Record<string, any> = {};
|
||||
|
||||
for (const collectionSlug of allowedCollections) {
|
||||
const slugKey = collectionSlug.replace(/-/g, '_')
|
||||
for (const collectionSlug of allowedCollections) {
|
||||
const slugKey = collectionSlug.replace(/-/g, "_");
|
||||
|
||||
// 1. Read (Find) Tool
|
||||
tools[`read_${slugKey}`] = tool({
|
||||
description: `Read/Find documents from the Payload CMS collection: ${collectionSlug}`,
|
||||
parameters: z.object({
|
||||
limit: z.number().optional().describe('Number of documents to return, max 100.'),
|
||||
page: z.number().optional().describe('Page number for pagination.'),
|
||||
// Simple string-based query for demo purposes. For a robust implementation,
|
||||
// we'd map this to Payload's where query logic using a structured Zod schema.
|
||||
query: z.string().optional().describe('Optional text to search within the collection.'),
|
||||
}),
|
||||
// @ts-ignore - AI SDK strict mode type inference bug
|
||||
execute: async ({ limit = 10, page = 1, query }: { limit?: number; page?: number; query?: string }) => {
|
||||
const where = query ? { id: { equals: query } } : undefined // Placeholder logic
|
||||
// 1. Read (Find) Tool
|
||||
tools[`read_${slugKey}`] = tool({
|
||||
description: `Read/Find documents from the Payload CMS collection: ${collectionSlug}`,
|
||||
parameters: z.object({
|
||||
limit: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Number of documents to return, max 100."),
|
||||
page: z.number().optional().describe("Page number for pagination."),
|
||||
// Simple string-based query for demo purposes. For a robust implementation,
|
||||
// we'd map this to Payload's where query logic using a structured Zod schema.
|
||||
query: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional text to search within the collection."),
|
||||
}),
|
||||
// @ts-expect-error - AI SDK strict mode type inference bug
|
||||
execute: async ({
|
||||
limit = 10,
|
||||
page = 1,
|
||||
query,
|
||||
}: {
|
||||
limit?: number;
|
||||
page?: number;
|
||||
query?: string;
|
||||
}) => {
|
||||
const where = query ? { id: { equals: query } } : undefined; // Placeholder logic
|
||||
|
||||
return await payload.find({
|
||||
collection: collectionSlug as any,
|
||||
limit: Math.min(limit, 100),
|
||||
page,
|
||||
where,
|
||||
req, // Crucial for passing the user context and respecting access control!
|
||||
})
|
||||
},
|
||||
})
|
||||
return await payload.find({
|
||||
collection: collectionSlug as any,
|
||||
limit: Math.min(limit, 100),
|
||||
page,
|
||||
where,
|
||||
req, // Crucial for passing the user context and respecting access control!
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 2. Read by ID Tool
|
||||
tools[`read_${slugKey}_by_id`] = tool({
|
||||
description: `Get a specific document by its ID from the ${collectionSlug} collection.`,
|
||||
parameters: z.object({
|
||||
id: z.union([z.string(), z.number()]).describe('The ID of the document.'),
|
||||
}),
|
||||
// @ts-ignore - AI SDK strict mode type inference bug
|
||||
execute: async ({ id }: { id: string | number }) => {
|
||||
return await payload.findByID({
|
||||
collection: collectionSlug as any,
|
||||
id,
|
||||
req, // Enforce access control
|
||||
})
|
||||
},
|
||||
})
|
||||
// 2. Read by ID Tool
|
||||
tools[`read_${slugKey}_by_id`] = tool({
|
||||
description: `Get a specific document by its ID from the ${collectionSlug} collection.`,
|
||||
parameters: z.object({
|
||||
id: z
|
||||
.union([z.string(), z.number()])
|
||||
.describe("The ID of the document."),
|
||||
}),
|
||||
// @ts-expect-error - AI SDK strict mode type inference bug
|
||||
execute: async ({ id }: { id: string | number }) => {
|
||||
return await payload.findByID({
|
||||
collection: collectionSlug as any,
|
||||
id,
|
||||
req, // Enforce access control
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 3. Create Tool
|
||||
tools[`create_${slugKey}`] = tool({
|
||||
description: `Create a new document in the ${collectionSlug} collection.`,
|
||||
parameters: z.object({
|
||||
data: z.record(z.any()).describe('A JSON object containing the data to insert.'),
|
||||
}),
|
||||
// @ts-ignore - AI SDK strict mode type inference bug
|
||||
execute: async ({ data }: { data: Record<string, any> }) => {
|
||||
return await payload.create({
|
||||
collection: collectionSlug as any,
|
||||
data,
|
||||
req, // Enforce access control
|
||||
})
|
||||
},
|
||||
})
|
||||
// 3. Create Tool
|
||||
tools[`create_${slugKey}`] = tool({
|
||||
description: `Create a new document in the ${collectionSlug} collection.`,
|
||||
parameters: z.object({
|
||||
data: z
|
||||
.record(z.any())
|
||||
.describe("A JSON object containing the data to insert."),
|
||||
}),
|
||||
// @ts-expect-error - AI SDK strict mode type inference bug
|
||||
execute: async ({ data }: { data: Record<string, any> }) => {
|
||||
return await payload.create({
|
||||
collection: collectionSlug as any,
|
||||
data,
|
||||
req, // Enforce access control
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 4. Update Tool
|
||||
tools[`update_${slugKey}`] = tool({
|
||||
description: `Update an existing document in the ${collectionSlug} collection.`,
|
||||
parameters: z.object({
|
||||
id: z.union([z.string(), z.number()]).describe('The ID of the document to update.'),
|
||||
data: z.record(z.any()).describe('A JSON object containing the fields to update.'),
|
||||
}),
|
||||
// @ts-ignore - AI SDK strict mode type inference bug
|
||||
execute: async ({ id, data }: { id: string | number; data: Record<string, any> }) => {
|
||||
return await payload.update({
|
||||
collection: collectionSlug as any,
|
||||
id,
|
||||
data,
|
||||
req, // Enforce access control
|
||||
})
|
||||
},
|
||||
})
|
||||
// 4. Update Tool
|
||||
tools[`update_${slugKey}`] = tool({
|
||||
description: `Update an existing document in the ${collectionSlug} collection.`,
|
||||
parameters: z.object({
|
||||
id: z
|
||||
.union([z.string(), z.number()])
|
||||
.describe("The ID of the document to update."),
|
||||
data: z
|
||||
.record(z.any())
|
||||
.describe("A JSON object containing the fields to update."),
|
||||
}),
|
||||
// @ts-expect-error - AI SDK strict mode type inference bug
|
||||
execute: async ({
|
||||
id,
|
||||
data,
|
||||
}: {
|
||||
id: string | number;
|
||||
data: Record<string, any>;
|
||||
}) => {
|
||||
return await payload.update({
|
||||
collection: collectionSlug as any,
|
||||
id,
|
||||
data,
|
||||
req, // Enforce access control
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 5. Delete Tool
|
||||
tools[`delete_${slugKey}`] = tool({
|
||||
description: `Delete a document from the ${collectionSlug} collection by ID.`,
|
||||
parameters: z.object({
|
||||
id: z.union([z.string(), z.number()]).describe('The ID of the document to delete.'),
|
||||
}),
|
||||
// @ts-ignore - AI SDK strict mode type inference bug
|
||||
execute: async ({ id }: { id: string | number }) => {
|
||||
return await payload.delete({
|
||||
collection: collectionSlug as any,
|
||||
id,
|
||||
req, // Enforce access control
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
// 5. Delete Tool
|
||||
tools[`delete_${slugKey}`] = tool({
|
||||
description: `Delete a document from the ${collectionSlug} collection by ID.`,
|
||||
parameters: z.object({
|
||||
id: z
|
||||
.union([z.string(), z.number()])
|
||||
.describe("The ID of the document to delete."),
|
||||
}),
|
||||
// @ts-expect-error - AI SDK strict mode type inference bug
|
||||
execute: async ({ id }: { id: string | number }) => {
|
||||
return await payload.delete({
|
||||
collection: collectionSlug as any,
|
||||
id,
|
||||
req, // Enforce access control
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return tools
|
||||
}
|
||||
return tools;
|
||||
};
|
||||
|
||||
15
packages/serpbear-mcp/Dockerfile
Normal file
15
packages/serpbear-mcp/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM node:20-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN corepack enable pnpm && pnpm install --ignore-workspace
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN pnpm build
|
||||
|
||||
FROM node:20-bookworm-slim
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
ENTRYPOINT ["node", "dist/start.js"]
|
||||
@@ -13,13 +13,12 @@
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"zod": "^3.23.8"
|
||||
"express": "^5.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^20.14.10",
|
||||
"typescript": "^5.5.3",
|
||||
"tsx": "^4.19.2"
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import express from 'express';
|
||||
import crypto from 'crypto';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
@@ -213,17 +214,34 @@ async function run() {
|
||||
console.error('SerpBear MCP server is running on stdio');
|
||||
} else {
|
||||
const app = express();
|
||||
let transport: SSEServerTransport | null = null;
|
||||
const transports = new Map<string, SSEServerTransport>();
|
||||
|
||||
app.use((req, _res, next) => {
|
||||
console.error(`${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/sse', async (req, res) => {
|
||||
console.error('New SSE connection established');
|
||||
transport = new SSEServerTransport('/message', res);
|
||||
const sessionId = crypto.randomUUID();
|
||||
console.error(`New SSE connection: ${sessionId}`);
|
||||
const transport = new SSEServerTransport(`/message/${sessionId}`, res);
|
||||
transports.set(sessionId, transport);
|
||||
|
||||
req.on('close', () => {
|
||||
console.error(`SSE connection closed: ${sessionId}`);
|
||||
transports.delete(sessionId);
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
});
|
||||
|
||||
app.post('/message', async (req, res) => {
|
||||
app.post('/message/:sessionId', async (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const transport = transports.get(sessionId as string);
|
||||
|
||||
if (!transport) {
|
||||
res.status(400).send('No active SSE connection');
|
||||
console.error(`No transport found for session: ${sessionId}`);
|
||||
res.status(400).send('No active SSE connection for this session');
|
||||
return;
|
||||
}
|
||||
await transport.handlePostMessage(req, res);
|
||||
|
||||
@@ -4,8 +4,8 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
config({ path: resolve(__dirname, '../../../.env.local') });
|
||||
config({ path: resolve(__dirname, '../../../.env') });
|
||||
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
|
||||
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
|
||||
|
||||
import('./index.js').catch(err => {
|
||||
console.error('Failed to start SerpBear MCP Server:', err);
|
||||
|
||||
15
packages/umami-mcp/Dockerfile
Normal file
15
packages/umami-mcp/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM node:20-bookworm-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN corepack enable pnpm && pnpm install --ignore-workspace
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN pnpm build
|
||||
|
||||
FROM node:20-bookworm-slim
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/package.json ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
ENTRYPOINT ["node", "dist/start.js"]
|
||||
@@ -13,13 +13,12 @@
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"zod": "^3.23.8"
|
||||
"express": "^5.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^20.14.10",
|
||||
"typescript": "^5.5.3",
|
||||
"tsx": "^4.19.2"
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import express from 'express';
|
||||
import crypto from 'crypto';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
@@ -251,17 +252,34 @@ async function run() {
|
||||
console.error('Umami MCP server is running on stdio');
|
||||
} else {
|
||||
const app = express();
|
||||
let transport: SSEServerTransport | null = null;
|
||||
const transports = new Map<string, SSEServerTransport>();
|
||||
|
||||
app.use((req, _res, next) => {
|
||||
console.error(`${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/sse', async (req, res) => {
|
||||
console.error('New SSE connection established');
|
||||
transport = new SSEServerTransport('/message', res);
|
||||
const sessionId = crypto.randomUUID();
|
||||
console.error(`New SSE connection: ${sessionId}`);
|
||||
const transport = new SSEServerTransport(`/message/${sessionId}`, res);
|
||||
transports.set(sessionId, transport);
|
||||
|
||||
req.on('close', () => {
|
||||
console.error(`SSE connection closed: ${sessionId}`);
|
||||
transports.delete(sessionId);
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
});
|
||||
|
||||
app.post('/message', async (req, res) => {
|
||||
app.post('/message/:sessionId', async (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const transport = transports.get(sessionId as string);
|
||||
|
||||
if (!transport) {
|
||||
res.status(400).send('No active SSE connection');
|
||||
console.error(`No transport found for session: ${sessionId}`);
|
||||
res.status(400).send('No active SSE connection for this session');
|
||||
return;
|
||||
}
|
||||
await transport.handlePostMessage(req, res);
|
||||
|
||||
@@ -4,8 +4,8 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
config({ path: resolve(__dirname, '../../../.env.local') });
|
||||
config({ path: resolve(__dirname, '../../../.env') });
|
||||
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
|
||||
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
|
||||
|
||||
import('./index.js').catch(err => {
|
||||
console.error('Failed to start Umami MCP Server:', err);
|
||||
|
||||
125
pnpm-lock.yaml
generated
125
pnpm-lock.yaml
generated
@@ -395,9 +395,6 @@ importers:
|
||||
express:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@types/express':
|
||||
specifier: ^5.0.6
|
||||
@@ -423,9 +420,6 @@ importers:
|
||||
express:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@types/express':
|
||||
specifier: ^5.0.6
|
||||
@@ -483,6 +477,52 @@ importers:
|
||||
specifier: ^5.0.0
|
||||
version: 5.9.3
|
||||
|
||||
packages/kabelfachmann-mcp:
|
||||
dependencies:
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: ^1.5.0
|
||||
version: 1.27.1(zod@3.25.76)
|
||||
'@qdrant/js-client-rest':
|
||||
specifier: ^1.12.0
|
||||
version: 1.17.0(typescript@5.9.3)
|
||||
'@xenova/transformers':
|
||||
specifier: ^2.17.2
|
||||
version: 2.17.2
|
||||
dotenv:
|
||||
specifier: ^17.3.1
|
||||
version: 17.3.1
|
||||
express:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
node-fetch:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
onnxruntime-node:
|
||||
specifier: ^1.14.0
|
||||
version: 1.14.0
|
||||
pdf-parse:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.4
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@types/express':
|
||||
specifier: ^5.0.6
|
||||
version: 5.0.6
|
||||
'@types/node':
|
||||
specifier: ^20.14.10
|
||||
version: 20.19.33
|
||||
'@types/pdf-parse':
|
||||
specifier: ^1.1.4
|
||||
version: 1.1.5
|
||||
tsx:
|
||||
specifier: ^4.19.1
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.5.3
|
||||
version: 5.9.3
|
||||
|
||||
packages/klz-payload-mcp:
|
||||
dependencies:
|
||||
'@modelcontextprotocol/sdk':
|
||||
@@ -497,9 +537,6 @@ importers:
|
||||
express:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@types/express':
|
||||
specifier: ^5.0.6
|
||||
@@ -584,6 +621,9 @@ importers:
|
||||
express:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
onnxruntime-node:
|
||||
specifier: ^1.14.0
|
||||
version: 1.14.0
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.25.76
|
||||
@@ -919,9 +959,6 @@ importers:
|
||||
express:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@types/express':
|
||||
specifier: ^5.0.6
|
||||
@@ -974,9 +1011,6 @@ importers:
|
||||
express:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@types/express':
|
||||
specifier: ^5.0.6
|
||||
@@ -3469,6 +3503,9 @@ packages:
|
||||
'@types/parse-json@4.0.2':
|
||||
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
|
||||
|
||||
'@types/pdf-parse@1.1.5':
|
||||
resolution: {integrity: sha512-kBfrSXsloMnUJOKi25s3+hRmkycHfLK6A09eRGqF/N8BkQoPUmaCr+q8Cli5FnfohEz/rsv82zAiPz/LXtOGhA==}
|
||||
|
||||
'@types/pg-pool@2.0.7':
|
||||
resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==}
|
||||
|
||||
@@ -4602,6 +4639,10 @@ packages:
|
||||
resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
data-uri-to-buffer@4.0.1:
|
||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
data-uri-to-buffer@6.0.2:
|
||||
resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -5190,6 +5231,10 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||
engines: {node: ^12.20 || >= 14.13}
|
||||
|
||||
fflate@0.8.2:
|
||||
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||
|
||||
@@ -5300,6 +5345,10 @@ packages:
|
||||
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
||||
engines: {node: '>= 12.20'}
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
forwarded-parse@2.1.2:
|
||||
resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
|
||||
|
||||
@@ -5422,6 +5471,7 @@ packages:
|
||||
git-raw-commits@4.0.0:
|
||||
resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==}
|
||||
engines: {node: '>=16'}
|
||||
deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead.
|
||||
hasBin: true
|
||||
|
||||
git-sha1@0.1.2:
|
||||
@@ -6521,6 +6571,9 @@ packages:
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
node-ensure@0.0.0:
|
||||
resolution: {integrity: sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
@@ -6530,6 +6583,10 @@ packages:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
node-fetch@3.3.2:
|
||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
node-releases@2.0.27:
|
||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||
|
||||
@@ -6791,6 +6848,10 @@ packages:
|
||||
peerDependencies:
|
||||
graphql: ^16.8.1
|
||||
|
||||
pdf-parse@1.1.4:
|
||||
resolution: {integrity: sha512-XRIRcLgk6ZnUbsHsYXExMw+krrPE81hJ6FQPLdBNhhBefqIQKXu/WeTgNBGSwPrfU0v+UCEwn7AoAUOsVKHFvQ==}
|
||||
engines: {node: '>=6.8.1'}
|
||||
|
||||
peberminta@0.9.0:
|
||||
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||
|
||||
@@ -8331,6 +8392,10 @@ packages:
|
||||
wcwidth@1.0.1:
|
||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3:
|
||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -11322,6 +11387,10 @@ snapshots:
|
||||
|
||||
'@types/parse-json@4.0.2': {}
|
||||
|
||||
'@types/pdf-parse@1.1.5':
|
||||
dependencies:
|
||||
'@types/node': 20.19.33
|
||||
|
||||
'@types/pg-pool@2.0.7':
|
||||
dependencies:
|
||||
'@types/pg': 8.15.6
|
||||
@@ -12540,6 +12609,8 @@ snapshots:
|
||||
|
||||
dargs@8.1.0: {}
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
||||
data-uri-to-buffer@6.0.2: {}
|
||||
|
||||
data-urls@5.0.0:
|
||||
@@ -13322,6 +13393,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 3.3.3
|
||||
|
||||
fflate@0.8.2: {}
|
||||
|
||||
figlet@1.10.0:
|
||||
@@ -13452,6 +13528,10 @@ snapshots:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 4.0.0-beta.3
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
dependencies:
|
||||
fetch-blob: 3.2.0
|
||||
|
||||
forwarded-parse@2.1.2: {}
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
@@ -14699,10 +14779,18 @@ snapshots:
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-ensure@0.0.0: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
node-fetch@3.3.2:
|
||||
dependencies:
|
||||
data-uri-to-buffer: 4.0.1
|
||||
fetch-blob: 3.2.0
|
||||
formdata-polyfill: 4.0.10
|
||||
|
||||
node-releases@2.0.27: {}
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
@@ -14794,7 +14882,6 @@ snapshots:
|
||||
onnxruntime-node@1.14.0:
|
||||
dependencies:
|
||||
onnxruntime-common: 1.14.0
|
||||
optional: true
|
||||
|
||||
onnxruntime-web@1.14.0:
|
||||
dependencies:
|
||||
@@ -15022,6 +15109,10 @@ snapshots:
|
||||
- typescript
|
||||
- utf-8-validate
|
||||
|
||||
pdf-parse@1.1.4:
|
||||
dependencies:
|
||||
node-ensure: 0.0.0
|
||||
|
||||
peberminta@0.9.0: {}
|
||||
|
||||
peek-readable@5.4.2: {}
|
||||
@@ -17005,6 +17096,8 @@ snapshots:
|
||||
dependencies:
|
||||
defaults: 1.0.4
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
Reference in New Issue
Block a user