ci: fix unhandled typescript exceptions and strict eslint errors caught by the pipeline
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 9s
Monorepo Pipeline / 🏗️ Build (push) Failing after 9s
Monorepo Pipeline / 🧪 Test (push) Failing after 9s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped

This commit is contained in:
2026-03-06 15:49:45 +01:00
parent 2dc61e4937
commit 61f2f83e0c
10 changed files with 1914 additions and 1223 deletions

View File

@@ -15,6 +15,8 @@ services:
build: build:
context: ./packages/gitea-mcp context: ./packages/gitea-mcp
container_name: gitea-mcp container_name: gitea-mcp
env_file:
- .env
ports: ports:
- "3001:3001" - "3001:3001"
restart: unless-stopped restart: unless-stopped
@@ -25,6 +27,8 @@ services:
build: build:
context: ./packages/memory-mcp context: ./packages/memory-mcp
container_name: memory-mcp container_name: memory-mcp
env_file:
- .env
ports: ports:
- "3002:3002" - "3002:3002"
depends_on: depends_on:
@@ -37,6 +41,8 @@ services:
build: build:
context: ./packages/umami-mcp context: ./packages/umami-mcp
container_name: umami-mcp container_name: umami-mcp
env_file:
- .env
ports: ports:
- "3003:3003" - "3003:3003"
restart: unless-stopped restart: unless-stopped
@@ -47,6 +53,8 @@ services:
build: build:
context: ./packages/serpbear-mcp context: ./packages/serpbear-mcp
container_name: serpbear-mcp container_name: serpbear-mcp
env_file:
- .env
ports: ports:
- "3004:3004" - "3004:3004"
restart: unless-stopped restart: unless-stopped
@@ -57,6 +65,8 @@ services:
build: build:
context: ./packages/glitchtip-mcp context: ./packages/glitchtip-mcp
container_name: glitchtip-mcp container_name: glitchtip-mcp
env_file:
- .env
ports: ports:
- "3005:3005" - "3005:3005"
restart: unless-stopped restart: unless-stopped
@@ -67,6 +77,8 @@ services:
build: build:
context: ./packages/klz-payload-mcp context: ./packages/klz-payload-mcp
container_name: klz-payload-mcp container_name: klz-payload-mcp
env_file:
- .env
ports: ports:
- "3006:3006" - "3006:3006"
restart: unless-stopped restart: unless-stopped

69
eslint-errors-2.txt Normal file
View File

@@ -0,0 +1,69 @@

/Users/marcmintel/Projects/at-mintel/packages/gitea-mcp/src/index.ts
 11:0 error Parsing error: Identifier expected

/Users/marcmintel/Projects/at-mintel/packages/glitchtip-mcp/src/index.ts
 124:19 warning 'res' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/klz-payload-mcp/src/index.ts
 39:18 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/memory-mcp/src/qdrant.test.ts
 7:52 warning 'text' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/page-audit/src/report.ts
 7:47 warning 'PageAuditData' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 7:62 warning 'AuditIssue' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/AiFieldButton.tsx
 11:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateSlugButton.tsx
 20:21 warning 'replaceState' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 21:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateThumbnailButton.tsx
 21:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/OptimizeButton.tsx
 5:10 warning 'Button' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/mcpAdapter.ts
 44:15 warning 'toolSchema' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/memoryDb.ts
 89:31 warning 'query' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/payloadLocal.ts
 3:40 warning 'User' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/types.ts
 1:15 warning 'Plugin' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/ConceptPDF.tsx
 4:18 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:10 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/EstimationPDF.tsx
 4:18 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:10 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 54:11 warning 'getPageNum' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/InfoPDF.tsx
 5:13 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 12:5 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SharedUI.tsx
 528:5 warning 'bankData' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SimpleLayout.tsx
 4:52 warning 'PDFText' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:26 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/seo-engine/src/report.ts
 5:3 warning 'TopicCluster' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 6:3 warning 'ContentGap' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 7:3 warning 'CompetitorRanking' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

✖ 28 problems (1 error, 27 warnings)


97
eslint-errors.txt Normal file
View File

@@ -0,0 +1,97 @@

/Users/marcmintel/Projects/at-mintel/packages/gitea-mcp/src/index.ts
 12:5 warning 'Resource' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 14:10 warning 'z' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 427:30 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars
 745:50 error Unnecessary escape character: \/ no-useless-escape
 745:60 error Unnecessary escape character: \/ no-useless-escape
 799:54 error Unnecessary escape character: \/ no-useless-escape
 799:64 error Unnecessary escape character: \/ no-useless-escape

/Users/marcmintel/Projects/at-mintel/packages/glitchtip-mcp/src/index.ts
 124:19 warning 'res' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/klz-payload-mcp/src/index.ts
 39:18 warning 'e' is defined but never used. Allowed unused caught errors must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/memory-mcp/src/qdrant.test.ts
 7:52 warning 'text' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/page-audit/src/report.ts
 7:47 warning 'PageAuditData' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 7:62 warning 'AuditIssue' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/chatPlugin.ts
 1:15 warning 'Config' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 10:17 error 'config' is never reassigned. Use 'const' instead prefer-const
 48:37 warning 'req' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/ChatWindow/index.tsx
 43:5 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 44:63 warning 'setMessages' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/AiFieldButton.tsx
 11:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateSlugButton.tsx
 20:21 warning 'replaceState' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 21:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/FieldGenerators/GenerateThumbnailButton.tsx
 21:13 warning 'value' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/components/OptimizeButton.tsx
 5:10 warning 'Button' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/endpoints/chatEndpoint.ts
 96:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 100:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/mcpAdapter.ts
 44:15 warning 'toolSchema' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 53:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/memoryDb.ts
 50:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 88:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 89:31 warning 'query' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/tools/payloadLocal.ts
 3:40 warning 'User' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 25:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 45:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 61:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 78:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
 95:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment

/Users/marcmintel/Projects/at-mintel/packages/payload-ai/src/types.ts
 1:15 warning 'Plugin' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/ConceptPDF.tsx
 4:18 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:10 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/EstimationPDF.tsx
 4:18 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:10 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 54:11 warning 'getPageNum' is assigned a value but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/InfoPDF.tsx
 5:13 warning 'PDFPage' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 12:5 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SharedUI.tsx
 528:5 warning 'bankData' is defined but never used. Allowed unused args must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/pdf-library/src/components/pdf/SimpleLayout.tsx
 4:52 warning 'PDFText' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 5:26 warning 'pdfStyles' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

/Users/marcmintel/Projects/at-mintel/packages/seo-engine/src/report.ts
 5:3 warning 'TopicCluster' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 6:3 warning 'ContentGap' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars
 7:3 warning 'CompetitorRanking' is defined but never used. Allowed unused vars must match /^_/u @typescript-eslint/no-unused-vars

✖ 49 problems (16 errors, 33 warnings)
 1 error and 0 warnings potentially fixable with the `--fix` option.


File diff suppressed because it is too large Load Diff

View File

@@ -1,76 +1,84 @@
import type { Config, Plugin } from 'payload' import type { Plugin } from "payload";
import { AIChatPermissionsCollection } from './collections/AIChatPermissions.js' import { AIChatPermissionsCollection } from "./collections/AIChatPermissions.js";
import type { PayloadChatPluginConfig } from './types.js' import type { PayloadChatPluginConfig } from "./types.js";
import { optimizePostEndpoint } from './endpoints/optimizeEndpoint.js' import { optimizePostEndpoint } from "./endpoints/optimizeEndpoint.js";
import { generateSlugEndpoint, generateThumbnailEndpoint, generateSingleFieldEndpoint } from './endpoints/generateEndpoints.js' import {
generateSlugEndpoint,
generateThumbnailEndpoint,
generateSingleFieldEndpoint,
} from "./endpoints/generateEndpoints.js";
export const payloadChatPlugin = export const payloadChatPlugin =
(pluginOptions: PayloadChatPluginConfig): Plugin => (pluginOptions: PayloadChatPluginConfig): Plugin =>
(incomingConfig) => { (incomingConfig) => {
let config = { ...incomingConfig } const config = { ...incomingConfig };
// If disabled, return config untouched // If disabled, return config untouched
if (pluginOptions.enabled === false) { if (pluginOptions.enabled === false) {
return config return config;
} }
// 1. Inject the Permissions Collection into the Schema // 1. Inject the Permissions Collection into the Schema
const existingCollections = config.collections || [] const existingCollections = config.collections || [];
const mcpServers = pluginOptions.mcpServers || [] const mcpServers = pluginOptions.mcpServers || [];
// Dynamically populate the select options for Collections and MCP Servers // Dynamically populate the select options for Collections and MCP Servers
const permissionCollection = { ...AIChatPermissionsCollection } const permissionCollection = { ...AIChatPermissionsCollection };
const collectionField = permissionCollection.fields.find(f => 'name' in f && f.name === 'allowedCollections') as any const collectionField = permissionCollection.fields.find(
(f) => "name" in f && f.name === "allowedCollections",
) as any;
if (collectionField) { if (collectionField) {
collectionField.options = existingCollections.map(c => ({ collectionField.options = existingCollections.map((c) => ({
label: c.labels?.singular || c.slug, label: c.labels?.singular || c.slug,
value: c.slug value: c.slug,
})) }));
} }
const mcpField = permissionCollection.fields.find(f => 'name' in f && f.name === 'allowedMcpServers') as any const mcpField = permissionCollection.fields.find(
(f) => "name" in f && f.name === "allowedMcpServers",
) as any;
if (mcpField) { if (mcpField) {
mcpField.options = mcpServers.map(s => ({ mcpField.options = mcpServers.map((s) => ({
label: s.name, label: s.name,
value: s.name value: s.name,
})) }));
} }
config.collections = [...existingCollections, permissionCollection] config.collections = [...existingCollections, permissionCollection];
// 2. Register Custom API Endpoint for the AI Chat // 2. Register Custom API Endpoint for the AI Chat
config.endpoints = [ config.endpoints = [
...(config.endpoints || []), ...(config.endpoints || []),
{ {
path: '/api/mcp-chat', path: "/api/mcp-chat",
method: 'post', method: "post",
handler: async (req) => { handler: async (_req) => {
// Fallback simple handler while developing endpoint logic // Fallback simple handler while developing endpoint logic
return Response.json({ message: "Chat endpoint active" }) return Response.json({ message: "Chat endpoint active" });
}, },
}, },
{ {
path: '/api/mintel-ai/optimize', path: "/api/mintel-ai/optimize",
method: 'post', method: "post",
handler: optimizePostEndpoint, handler: optimizePostEndpoint,
}, },
{ {
path: '/api/mintel-ai/generate-slug', path: "/api/mintel-ai/generate-slug",
method: 'post', method: "post",
handler: generateSlugEndpoint, handler: generateSlugEndpoint,
}, },
{ {
path: '/api/mintel-ai/generate-thumbnail', path: "/api/mintel-ai/generate-thumbnail",
method: 'post', method: "post",
handler: generateThumbnailEndpoint, handler: generateThumbnailEndpoint,
}, },
{ {
path: '/api/mintel-ai/generate-single-field', path: "/api/mintel-ai/generate-single-field",
method: 'post', method: "post",
handler: generateSingleFieldEndpoint, handler: generateSingleFieldEndpoint,
}, },
] ];
// 3. Inject Chat React Component into Admin UI // 3. Inject Chat React Component into Admin UI
if (pluginOptions.renderChatBubble !== false) { if (pluginOptions.renderChatBubble !== false) {
@@ -80,11 +88,11 @@ export const payloadChatPlugin =
...(config.admin?.components || {}), ...(config.admin?.components || {}),
providers: [ providers: [
...(config.admin?.components?.providers || []), ...(config.admin?.components?.providers || []),
'@mintel/payload-ai/components/ChatWindow#ChatWindowProvider', "@mintel/payload-ai/components/ChatWindow#ChatWindowProvider",
], ],
}, },
} };
} }
return config return config;
} };

View File

@@ -1,24 +1,26 @@
'use client' "use client";
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from "react";
import { useChat } from '@ai-sdk/react' import { useChat } from "@ai-sdk/react";
import './ChatWindow.scss' import "./ChatWindow.scss";
export const ChatWindowProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const ChatWindowProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
return ( return (
<> <>
{children} {children}
<ChatWindow /> <ChatWindow />
</> </>
) );
} };
const ChatWindow: React.FC = () => { const ChatWindow: React.FC = () => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false);
const [pageContext, setPageContext] = useState<any>({ url: '' }) const [pageContext, setPageContext] = useState<any>({ url: "" });
useEffect(() => { useEffect(() => {
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
const path = window.location.pathname; const path = window.location.pathname;
let collectionSlug = null; let collectionSlug = null;
let id = null; let id = null;
@@ -26,7 +28,7 @@ const ChatWindow: React.FC = () => {
const match = path.match(/\/collections\/([^/]+)(?:\/([^/]+))?/); const match = path.match(/\/collections\/([^/]+)(?:\/([^/]+))?/);
if (match) { if (match) {
collectionSlug = match[1]; collectionSlug = match[1];
if (match[2] && match[2] !== 'create') { if (match[2] && match[2] !== "create") {
id = match[2]; id = match[2];
} }
} }
@@ -35,19 +37,19 @@ const ChatWindow: React.FC = () => {
url: window.location.href, url: window.location.href,
title: document.title, title: document.title,
collectionSlug, collectionSlug,
id id,
}) });
} }
}, [isOpen]) // Refresh context when chat is opened }, [isOpen]); // Refresh context when chat is opened
// @ts-ignore - AI hook version mismatch between core and react packages // @ts-expect-error - AI hook version mismatch between core and react packages
const { messages, input, handleInputChange, handleSubmit, setMessages } = useChat({ const { messages, input, handleInputChange, handleSubmit } = useChat({
api: '/api/mcp-chat', api: "/api/mcp-chat",
initialMessages: [], initialMessages: [],
body: { body: {
pageContext pageContext,
} },
} as any) } as any);
// Basic implementation to toggle chat window and submit messages // Basic implementation to toggle chat window and submit messages
return ( return (
@@ -56,81 +58,101 @@ const ChatWindow: React.FC = () => {
className="payload-mcp-chat-toggle" className="payload-mcp-chat-toggle"
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
style={{ style={{
position: 'fixed', position: "fixed",
bottom: '20px', bottom: "20px",
right: '20px', right: "20px",
zIndex: 9999, zIndex: 9999,
padding: '12px 24px', padding: "12px 24px",
backgroundColor: '#000', backgroundColor: "#000",
color: '#fff', color: "#fff",
borderRadius: '8px', borderRadius: "8px",
border: 'none', border: "none",
cursor: 'pointer', cursor: "pointer",
fontWeight: 'bold' fontWeight: "bold",
}} }}
> >
{isOpen ? 'Close AI Chat' : 'Ask AI'} {isOpen ? "Close AI Chat" : "Ask AI"}
</button> </button>
{isOpen && ( {isOpen && (
<div <div
className="payload-mcp-chat-window" className="payload-mcp-chat-window"
style={{ style={{
position: 'fixed', position: "fixed",
bottom: '80px', bottom: "80px",
right: '20px', right: "20px",
width: '450px', width: "450px",
height: '650px', height: "650px",
backgroundColor: '#fff', backgroundColor: "#fff",
border: '1px solid #eaeaea', border: "1px solid #eaeaea",
borderRadius: '12px', borderRadius: "12px",
zIndex: 9999, zIndex: 9999,
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
boxShadow: '0 10px 40px rgba(0,0,0,0.1)' 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' }}> <div
<h3 style={{ margin: 0, fontSize: '16px' }}>Payload MCP Chat</h3> 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>
<div className="chat-messages" style={{ flex: 1, padding: '16px', overflowY: 'auto' }}> <div
className="chat-messages"
style={{ flex: 1, padding: "16px", overflowY: "auto" }}
>
{messages.map((m: any) => ( {messages.map((m: any) => (
<div key={m.id} style={{ <div
marginBottom: '12px', key={m.id}
textAlign: m.role === 'user' ? 'right' : 'left' style={{
}}> marginBottom: "12px",
<div style={{ textAlign: m.role === "user" ? "right" : "left",
display: 'inline-block', }}
padding: '8px 12px', >
borderRadius: '8px', <div
backgroundColor: m.role === 'user' ? '#000' : '#f0f0f0', style={{
color: m.role === 'user' ? '#fff' : '#000', display: "inline-block",
maxWidth: '80%' padding: "8px 12px",
}}> borderRadius: "8px",
{m.role === 'user' ? 'G: ' : 'AI: '} backgroundColor: m.role === "user" ? "#000" : "#f0f0f0",
color: m.role === "user" ? "#fff" : "#000",
maxWidth: "80%",
}}
>
{m.role === "user" ? "G: " : "AI: "}
{m.content} {m.content}
</div> </div>
</div> </div>
))} ))}
</div> </div>
<form onSubmit={handleSubmit} style={{ padding: '16px', borderTop: '1px solid #eaeaea' }}> <form
onSubmit={handleSubmit}
style={{ padding: "16px", borderTop: "1px solid #eaeaea" }}
>
<input <input
value={input} value={input}
placeholder="Ask me anything or use /commands..." placeholder="Ask me anything or use /commands..."
onChange={handleInputChange} onChange={handleInputChange}
style={{ style={{
width: '100%', width: "100%",
padding: '12px', padding: "12px",
borderRadius: '8px', borderRadius: "8px",
border: '1px solid #eaeaea', border: "1px solid #eaeaea",
boxSizing: 'border-box' boxSizing: "border-box",
}} }}
/> />
</form> </form>
</div> </div>
)} )}
</div> </div>
) );
} };

View File

@@ -1,115 +1,143 @@
import { streamText } from 'ai' import { streamText } from "ai";
import { createOpenAI } from '@ai-sdk/openai' import { createOpenAI } from "@ai-sdk/openai";
import { generatePayloadLocalTools } from '../tools/payloadLocal.js' import { generatePayloadLocalTools } from "../tools/payloadLocal.js";
import { createMcpTools } from '../tools/mcpAdapter.js' import { createMcpTools } from "../tools/mcpAdapter.js";
import { generateMemoryTools } from '../tools/memoryDb.js' import { generateMemoryTools } from "../tools/memoryDb.js";
import type { PayloadRequest } from 'payload' import type { PayloadRequest } from "payload";
const openrouter = createOpenAI({ const openrouter = createOpenAI({
baseURL: 'https://openrouter.ai/api/v1', baseURL: "https://openrouter.ai/api/v1",
apiKey: process.env.OPENROUTER_API_KEY || 'dummy_key', apiKey: process.env.OPENROUTER_API_KEY || "dummy_key",
}) });
export const handleMcpChat = async (req: PayloadRequest) => { export const handleMcpChat = async (req: PayloadRequest) => {
if (!req.user) { if (!req.user) {
return Response.json({ error: 'Unauthorized. You must be logged in to use AI Chat.' }, { status: 401 }) 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 } const { messages, pageContext } = ((await req.json?.()) || {
messages: [],
}) as { messages: any[]; pageContext?: any };
// 1. Check AI Permissions for req.user // 1. Check AI Permissions for req.user
// Look up the collection for permissions // Look up the collection for permissions
const permissionsQuery = await req.payload.find({ const permissionsQuery = await req.payload.find({
collection: 'ai-chat-permissions' as any, collection: "ai-chat-permissions" as any,
where: { where: {
or: [ or: [
{ targetUser: { equals: req.user.id } }, { targetUser: { equals: req.user.id } },
{ targetRole: { equals: req.user.role || 'admin' } } { targetRole: { equals: req.user.role || "admin" } },
] ],
}, },
limit: 10 limit: 10,
}) });
const allowedCollections = new Set<string>() const allowedCollections = new Set<string>();
const allowedMcpServers = new Set<string>() const allowedMcpServers = new Set<string>();
for (const perm of permissionsQuery.docs) { for (const perm of permissionsQuery.docs) {
if (perm.allowedCollections) { if (perm.allowedCollections) {
perm.allowedCollections.forEach((c: string) => allowedCollections.add(c)) perm.allowedCollections.forEach((c: string) => allowedCollections.add(c));
} }
if (perm.allowedMcpServers) { if (perm.allowedMcpServers) {
perm.allowedMcpServers.forEach((s: string) => allowedMcpServers.add(s)) perm.allowedMcpServers.forEach((s: string) => allowedMcpServers.add(s));
} }
} }
let accessCollections = Array.from(allowedCollections) let accessCollections = Array.from(allowedCollections);
if (accessCollections.length === 0) { if (accessCollections.length === 0) {
// Fallback or demo config if not configured yet // Fallback or demo config if not configured yet
accessCollections = ['users', 'pages', 'posts', 'products', 'leads', 'media'] accessCollections = [
"users",
"pages",
"posts",
"products",
"leads",
"media",
];
} }
let activeTools: Record<string, any> = {} let activeTools: Record<string, any> = {};
// 2. Generate Payload Local Tools // 2. Generate Payload Local Tools
if (accessCollections.length > 0) { if (accessCollections.length > 0) {
const payloadTools = generatePayloadLocalTools(req.payload, req, accessCollections) const payloadTools = generatePayloadLocalTools(
activeTools = { ...activeTools, ...payloadTools } req.payload,
req,
accessCollections,
);
activeTools = { ...activeTools, ...payloadTools };
} }
// 3. Connect External MCPs // 3. Connect External MCPs
if (Array.from(allowedMcpServers).includes('gitea')) { if (Array.from(allowedMcpServers).includes("gitea")) {
try { try {
const { tools: giteaTools } = await createMcpTools({ const { tools: giteaTools } = await createMcpTools({
name: 'gitea', name: "gitea",
command: 'npx', command: "npx",
args: ['-y', '@modelcontextprotocol/server-gitea', '--url', 'https://git.mintel.int', '--token', process.env.GITEA_TOKEN || ''] args: [
}) "-y",
activeTools = { ...activeTools, ...giteaTools } "@modelcontextprotocol/server-gitea",
"--url",
"https://git.mintel.int",
"--token",
process.env.GITEA_TOKEN || "",
],
});
activeTools = { ...activeTools, ...giteaTools };
} catch (e) { } catch (e) {
console.error('Failed to connect to Gitea MCP', e) console.error("Failed to connect to Gitea MCP", e);
} }
} }
// 4. Inject Memory Database Tools // 4. Inject Memory Database Tools
// We provide the user ID so memory is partitioned per user // We provide the user ID so memory is partitioned per user
const memoryTools = generateMemoryTools(req.user.id) const memoryTools = generateMemoryTools(req.user.id);
activeTools = { ...activeTools, ...memoryTools } activeTools = { ...activeTools, ...memoryTools };
// 5. Build prompt to ensure it asks before saving // 5. Build prompt to ensure it asks before saving
const memorySystemPrompt = ` const memorySystemPrompt = `
You have access to a long-term vector memory database (Qdrant). 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 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. 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: Current User Context:
URL: ${pageContext.url || 'Unknown'} URL: ${pageContext.url || "Unknown"}
Title: ${pageContext.title || 'Unknown'} Title: ${pageContext.title || "Unknown"}
Collection: ${pageContext.collectionSlug || 'None'} Collection: ${pageContext.collectionSlug || "None"}
Document ID: ${pageContext.id || 'None'} Document ID: ${pageContext.id || "None"}
You can use this to understand what the user is currently looking at. You can use this to understand what the user is currently looking at.
` : '' `
: "";
try { try {
const result = streamText({ const result = streamText({
// @ts-ignore - AI SDK type mismatch // @ts-expect-error - AI SDK type mismatch
model: openrouter('google/gemini-3.0-flash'), model: openrouter("google/gemini-3.0-flash"),
messages, messages,
tools: activeTools, tools: activeTools,
// @ts-ignore - AI SDK type mismatch with maxSteps // @ts-expect-error - AI SDK type mismatch with maxSteps
maxSteps: 10, maxSteps: 10,
system: `You are a helpful Payload CMS Agent orchestrating the local Mintel ecosystem. 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 only have access to tools explicitly granted by the Admin.
You can completely control Payload CMS (read, create, update, delete documents). 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. If you need more details to fulfill a request (e.g. creating a blog post), you can ask the user.
${contextContextStr} ${contextContextStr}
${memorySystemPrompt}` ${memorySystemPrompt}`,
}) });
return result.toTextStreamResponse() return result.toTextStreamResponse();
} catch (error) { } catch (error) {
console.error("AI Error:", error) console.error("AI Error:", error);
return Response.json({ error: 'Failed to process AI request' }, { status: 500 }) return Response.json(
{ error: "Failed to process AI request" },
{ status: 500 },
);
} }
} };

View File

@@ -1,39 +1,44 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { tool } from 'ai' import { tool } from "ai";
import { z } from 'zod' import { z } from "zod";
/** /**
* Connects to an external MCP Server and maps its tools to Vercel AI SDK Tools. * 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[] }) { export async function createMcpTools(mcpConfig: {
let transport name: string;
url?: string;
command?: string;
args?: string[];
}) {
let transport;
// Support both HTTP/SSE and STDIO transports // Support both HTTP/SSE and STDIO transports
if (mcpConfig.url) { if (mcpConfig.url) {
transport = new SSEClientTransport(new URL(mcpConfig.url)) transport = new SSEClientTransport(new URL(mcpConfig.url));
} else if (mcpConfig.command) { } else if (mcpConfig.command) {
transport = new StdioClientTransport({ transport = new StdioClientTransport({
command: mcpConfig.command, command: mcpConfig.command,
args: mcpConfig.args || [], args: mcpConfig.args || [],
}) });
} else { } else {
throw new Error('Invalid MCP config: Must provide either URL or Command.') throw new Error("Invalid MCP config: Must provide either URL or Command.");
} }
const client = new Client( const client = new Client(
{ name: `payload-ai-client-${mcpConfig.name}`, version: '1.0.0' }, { name: `payload-ai-client-${mcpConfig.name}`, version: "1.0.0" },
{ capabilities: {} } { capabilities: {} },
) );
await client.connect(transport) await client.connect(transport);
// Fetch available tools from the external MCP server // Fetch available tools from the external MCP server
const toolListResult = await client.listTools() const toolListResult = await client.listTools();
const externalTools = toolListResult.tools || [] const externalTools = toolListResult.tools || [];
const aiSdkTools: Record<string, any> = {} const aiSdkTools: Record<string, any> = {};
// Map each external tool to a Vercel AI SDK Tool // Map each external tool to a Vercel AI SDK Tool
for (const extTool of externalTools) { for (const extTool of externalTools) {
@@ -41,7 +46,7 @@ export async function createMcpTools(mcpConfig: { name: string, url?: string, co
// Note: For a production ready adapter, you might need a more robust jsonSchemaToZod converter // 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. // 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. // 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> const toolSchema = extTool.inputSchema as Record<string, any>;
// We create a simplified parameter parser. // We create a simplified parameter parser.
// An ideal approach uses `jsonSchemaToZod` library or native AI SDK JSON schema support // An ideal approach uses `jsonSchemaToZod` library or native AI SDK JSON schema support
@@ -49,17 +54,19 @@ export async function createMcpTools(mcpConfig: { name: string, url?: string, co
aiSdkTools[`${mcpConfig.name}_${extTool.name}`] = tool({ aiSdkTools[`${mcpConfig.name}_${extTool.name}`] = tool({
description: `[From ${mcpConfig.name}] ${extTool.description || extTool.name}`, description: `[From ${mcpConfig.name}] ${extTool.description || extTool.name}`,
parameters: z.any().describe('JSON matching the original MCP input_schema'), // Simplify for prototype parameters: z
// @ts-ignore - AI strict mode overload bug with implicit zod inferences .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) => { execute: async (args: any) => {
const result = await client.callTool({ const result = await client.callTool({
name: extTool.name, name: extTool.name,
arguments: args arguments: args,
}) });
return result return result;
} },
}) });
} }
return { tools: aiSdkTools, client } return { tools: aiSdkTools, client };
} }

View File

@@ -1,37 +1,39 @@
import { tool } from 'ai' import { tool } from "ai";
import { z } from 'zod' import { z } from "zod";
import { QdrantClient } from '@qdrant/js-client-rest' import { QdrantClient } from "@qdrant/js-client-rest";
// Qdrant initialization // Qdrant initialization
// This requires the user to have Qdrant running and QDRANT_URL/QDRANT_API_KEY environment variables set // This requires the user to have Qdrant running and QDRANT_URL/QDRANT_API_KEY environment variables set
const qdrantClient = new QdrantClient({ const qdrantClient = new QdrantClient({
url: process.env.QDRANT_URL || 'http://localhost:6333', url: process.env.QDRANT_URL || "http://localhost:6333",
apiKey: process.env.QDRANT_API_KEY, apiKey: process.env.QDRANT_API_KEY,
}) });
const MEMORY_COLLECTION = 'mintel_ai_memory' const MEMORY_COLLECTION = "mintel_ai_memory";
// Ensure collection exists on load // Ensure collection exists on load
async function initQdrant() { async function initQdrant() {
try { try {
const res = await qdrantClient.getCollections() const res = await qdrantClient.getCollections();
const exists = res.collections.find((c: any) => c.name === MEMORY_COLLECTION) const exists = res.collections.find(
(c: any) => c.name === MEMORY_COLLECTION,
);
if (!exists) { if (!exists) {
await qdrantClient.createCollection(MEMORY_COLLECTION, { await qdrantClient.createCollection(MEMORY_COLLECTION, {
vectors: { vectors: {
size: 1536, // typical embedding size, adjust based on the embedding model used size: 1536, // typical embedding size, adjust based on the embedding model used
distance: 'Cosine', distance: "Cosine",
}, },
}) });
console.log(`Qdrant collection '${MEMORY_COLLECTION}' created.`) console.log(`Qdrant collection '${MEMORY_COLLECTION}' created.`);
} }
} catch (error) { } catch (error) {
console.error('Failed to initialize Qdrant memory collection:', error) console.error("Failed to initialize Qdrant memory collection:", error);
} }
} }
// Call init, but don't block // Call init, but don't block
initQdrant() initQdrant();
/** /**
* Returns memory tools for the AI SDK. * Returns memory tools for the AI SDK.
@@ -42,20 +44,34 @@ initQdrant()
export const generateMemoryTools = (userId: string | number) => { export const generateMemoryTools = (userId: string | number) => {
return { return {
save_memory: tool({ 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.', 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({ parameters: z.object({
fact: z.string().describe('The fact or instruction to remember.'), fact: z.string().describe("The fact or instruction to remember."),
category: z.string().optional().describe('An optional category like "preference", "rule", or "project_detail".'), category: z
.string()
.optional()
.describe(
'An optional category like "preference", "rule", or "project_detail".',
),
}), }),
// @ts-ignore - AI SDK strict mode bug // @ts-expect-error - AI SDK strict mode bug
execute: async ({ fact, category }: { fact: string; category?: string }) => { execute: async ({
fact,
category,
}: {
fact: string;
category?: string;
}) => {
// In a real scenario, you MUST generate embeddings for the 'fact' string here // In a real scenario, you MUST generate embeddings for the 'fact' string here
// using OpenAI or another embedding provider before inserting into Qdrant. // using OpenAI or another embedding provider before inserting into Qdrant.
// const embedding = await generateEmbedding(fact) // const embedding = await generateEmbedding(fact)
try { try {
// Mock embedding payload for demonstration // Mock embedding payload for demonstration
const mockEmbedding = new Array(1536).fill(0).map(() => Math.random()) const mockEmbedding = new Array(1536)
.fill(0)
.map(() => Math.random());
await qdrantClient.upsert(MEMORY_COLLECTION, { await qdrantClient.upsert(MEMORY_COLLECTION, {
wait: true, wait: true,
@@ -71,24 +87,33 @@ export const generateMemoryTools = (userId: string | number) => {
}, },
}, },
], ],
}) });
return { success: true, message: `Successfully remembered: "${fact}"` } return {
success: true,
message: `Successfully remembered: "${fact}"`,
};
} catch (error) { } catch (error) {
console.error("Qdrant save error:", error) console.error("Qdrant save error:", error);
return { success: false, error: 'Failed to save to memory database.' } return {
success: false,
error: "Failed to save to memory database.",
};
} }
}, },
}), }),
search_memory: tool({ search_memory: tool({
description: 'Search the user\'s long-term memory for past factual context, preferences, or rules.', description:
"Search the user's long-term memory for past factual context, preferences, or rules.",
parameters: z.object({ parameters: z.object({
query: z.string().describe('The search string to find in memory.'), query: z.string().describe("The search string to find in memory."),
}), }),
// @ts-ignore - AI SDK strict mode bug // @ts-expect-error - AI SDK strict mode bug
execute: async ({ query }: { query: string }) => { execute: async ({ query }: { query: string }) => {
// Generate embedding for query // Generate embedding for query
const mockQueryEmbedding = new Array(1536).fill(0).map(() => Math.random()) const mockQueryEmbedding = new Array(1536)
.fill(0)
.map(() => Math.random());
try { try {
const results = await qdrantClient.search(MEMORY_COLLECTION, { const results = await qdrantClient.search(MEMORY_COLLECTION, {
@@ -97,19 +122,19 @@ export const generateMemoryTools = (userId: string | number) => {
filter: { filter: {
must: [ must: [
{ {
key: 'userId', key: "userId",
match: { value: String(userId) } match: { value: String(userId) },
} },
] ],
} },
}) });
return results.map((r: any) => r.payload?.fact || '') return results.map((r: any) => r.payload?.fact || "");
} catch (error) { } catch (error) {
console.error("Qdrant search error:", error) console.error("Qdrant search error:", error);
return [] return [];
} }
} },
}) }),
} };
} };

View File

@@ -1,30 +1,44 @@
import { tool } from 'ai' import { tool } from "ai";
import { z } from 'zod' import { z } from "zod";
import type { Payload, PayloadRequest, User } from 'payload' import type { Payload, PayloadRequest, User } from "payload";
export const generatePayloadLocalTools = ( export const generatePayloadLocalTools = (
payload: Payload, payload: Payload,
req: PayloadRequest, req: PayloadRequest,
allowedCollections: string[] allowedCollections: string[],
) => { ) => {
const tools: Record<string, any> = {} const tools: Record<string, any> = {};
for (const collectionSlug of allowedCollections) { for (const collectionSlug of allowedCollections) {
const slugKey = collectionSlug.replace(/-/g, '_') const slugKey = collectionSlug.replace(/-/g, "_");
// 1. Read (Find) Tool // 1. Read (Find) Tool
tools[`read_${slugKey}`] = tool({ tools[`read_${slugKey}`] = tool({
description: `Read/Find documents from the Payload CMS collection: ${collectionSlug}`, description: `Read/Find documents from the Payload CMS collection: ${collectionSlug}`,
parameters: z.object({ parameters: z.object({
limit: z.number().optional().describe('Number of documents to return, max 100.'), limit: z
page: z.number().optional().describe('Page number for pagination.'), .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, // 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. // 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.'), query: z
.string()
.optional()
.describe("Optional text to search within the collection."),
}), }),
// @ts-ignore - AI SDK strict mode type inference bug // @ts-expect-error - AI SDK strict mode type inference bug
execute: async ({ limit = 10, page = 1, query }: { limit?: number; page?: number; query?: string }) => { execute: async ({
const where = query ? { id: { equals: query } } : undefined // Placeholder logic limit = 10,
page = 1,
query,
}: {
limit?: number;
page?: number;
query?: string;
}) => {
const where = query ? { id: { equals: query } } : undefined; // Placeholder logic
return await payload.find({ return await payload.find({
collection: collectionSlug as any, collection: collectionSlug as any,
@@ -32,76 +46,92 @@ export const generatePayloadLocalTools = (
page, page,
where, where,
req, // Crucial for passing the user context and respecting access control! req, // Crucial for passing the user context and respecting access control!
}) });
}, },
}) });
// 2. Read by ID Tool // 2. Read by ID Tool
tools[`read_${slugKey}_by_id`] = tool({ tools[`read_${slugKey}_by_id`] = tool({
description: `Get a specific document by its ID from the ${collectionSlug} collection.`, description: `Get a specific document by its ID from the ${collectionSlug} collection.`,
parameters: z.object({ parameters: z.object({
id: z.union([z.string(), z.number()]).describe('The ID of the document.'), id: z
.union([z.string(), z.number()])
.describe("The ID of the document."),
}), }),
// @ts-ignore - AI SDK strict mode type inference bug // @ts-expect-error - AI SDK strict mode type inference bug
execute: async ({ id }: { id: string | number }) => { execute: async ({ id }: { id: string | number }) => {
return await payload.findByID({ return await payload.findByID({
collection: collectionSlug as any, collection: collectionSlug as any,
id, id,
req, // Enforce access control req, // Enforce access control
}) });
}, },
}) });
// 3. Create Tool // 3. Create Tool
tools[`create_${slugKey}`] = tool({ tools[`create_${slugKey}`] = tool({
description: `Create a new document in the ${collectionSlug} collection.`, description: `Create a new document in the ${collectionSlug} collection.`,
parameters: z.object({ parameters: z.object({
data: z.record(z.any()).describe('A JSON object containing the data to insert.'), data: z
.record(z.any())
.describe("A JSON object containing the data to insert."),
}), }),
// @ts-ignore - AI SDK strict mode type inference bug // @ts-expect-error - AI SDK strict mode type inference bug
execute: async ({ data }: { data: Record<string, any> }) => { execute: async ({ data }: { data: Record<string, any> }) => {
return await payload.create({ return await payload.create({
collection: collectionSlug as any, collection: collectionSlug as any,
data, data,
req, // Enforce access control req, // Enforce access control
}) });
}, },
}) });
// 4. Update Tool // 4. Update Tool
tools[`update_${slugKey}`] = tool({ tools[`update_${slugKey}`] = tool({
description: `Update an existing document in the ${collectionSlug} collection.`, description: `Update an existing document in the ${collectionSlug} collection.`,
parameters: z.object({ parameters: z.object({
id: z.union([z.string(), z.number()]).describe('The ID of the document to update.'), id: z
data: z.record(z.any()).describe('A JSON object containing the fields to update.'), .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 // @ts-expect-error - AI SDK strict mode type inference bug
execute: async ({ id, data }: { id: string | number; data: Record<string, any> }) => { execute: async ({
id,
data,
}: {
id: string | number;
data: Record<string, any>;
}) => {
return await payload.update({ return await payload.update({
collection: collectionSlug as any, collection: collectionSlug as any,
id, id,
data, data,
req, // Enforce access control req, // Enforce access control
}) });
}, },
}) });
// 5. Delete Tool // 5. Delete Tool
tools[`delete_${slugKey}`] = tool({ tools[`delete_${slugKey}`] = tool({
description: `Delete a document from the ${collectionSlug} collection by ID.`, description: `Delete a document from the ${collectionSlug} collection by ID.`,
parameters: z.object({ parameters: z.object({
id: z.union([z.string(), z.number()]).describe('The ID of the document to delete.'), id: z
.union([z.string(), z.number()])
.describe("The ID of the document to delete."),
}), }),
// @ts-ignore - AI SDK strict mode type inference bug // @ts-expect-error - AI SDK strict mode type inference bug
execute: async ({ id }: { id: string | number }) => { execute: async ({ id }: { id: string | number }) => {
return await payload.delete({ return await payload.delete({
collection: collectionSlug as any, collection: collectionSlug as any,
id, id,
req, // Enforce access control req, // Enforce access control
}) });
}, },
}) });
} }
return tools return tools;
} };