diff --git a/.gitea/workflows/pipeline.yml b/.gitea/workflows/pipeline.yml index 5a38d36..445aaa2 100644 --- a/.gitea/workflows/pipeline.yml +++ b/.gitea/workflows/pipeline.yml @@ -202,9 +202,9 @@ jobs: - name: πŸ” Registry Login uses: docker/login-action@v3 with: - registry: registry.infra.mintel.me - username: ${{ secrets.REGISTRY_USER }} - password: ${{ secrets.REGISTRY_PASS }} + registry: git.infra.mintel.me + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: πŸ—οΈ Build & Push ${{ matrix.name }} uses: docker/build-push-action@v5 @@ -218,6 +218,6 @@ jobs: secrets: | NPM_TOKEN=${{ secrets.NPM_TOKEN }} tags: | - registry.infra.mintel.me/mintel/${{ matrix.image }}:${{ github.ref_name }} - registry.infra.mintel.me/mintel/${{ matrix.image }}:latest + git.infra.mintel.me/mmintel/${{ matrix.image }}:${{ github.ref_name }} + git.infra.mintel.me/mmintel/${{ matrix.image }}:latest diff --git a/Dockerfile.template b/Dockerfile.template index 624effb..ac5518a 100644 --- a/Dockerfile.template +++ b/Dockerfile.template @@ -1,5 +1,5 @@ # Stage 1: Builder -FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder +FROM git.infra.mintel.me/mmintel/nextjs:latest AS builder WORKDIR /app # Clean the workspace in case the base image is dirty @@ -37,7 +37,7 @@ COPY . . RUN pnpm build # Stage 2: Runner -FROM registry.infra.mintel.me/mintel/runtime:latest AS runner +FROM git.infra.mintel.me/mmintel/runtime:latest AS runner WORKDIR /app ENV HOSTNAME="0.0.0.0" diff --git a/packages/infra/docker/Dockerfile.app-template b/packages/infra/docker/Dockerfile.app-template index 6ebfe1f..b4947e7 100644 --- a/packages/infra/docker/Dockerfile.app-template +++ b/packages/infra/docker/Dockerfile.app-template @@ -1,5 +1,5 @@ # Start from the pre-built Nextjs Base image -FROM registry.infra.mintel.me/mintel/nextjs:latest AS builder +FROM git.infra.mintel.me/mmintel/nextjs:latest AS builder WORKDIR /app @@ -20,7 +20,7 @@ ENV DIRECTUS_URL=$DIRECTUS_URL RUN pnpm --filter ${APP_NAME:-app} build # Production runner image -FROM registry.infra.mintel.me/mintel/runtime:latest AS runner +FROM git.infra.mintel.me/mmintel/runtime:latest AS runner WORKDIR /app # Copy standalone output and static files diff --git a/packages/infra/docker/docker-compose.template.yml b/packages/infra/docker/docker-compose.template.yml index 0694137..f872a47 100644 --- a/packages/infra/docker/docker-compose.template.yml +++ b/packages/infra/docker/docker-compose.template.yml @@ -38,7 +38,7 @@ services: - "traefik.http.middlewares.${PROJECT_NAME:-app}-auth.forwardauth.authResponseHeaders=X-Auth-User" gatekeeper: - image: registry.infra.mintel.me/mintel/gatekeeper:${IMAGE_TAG:-latest} + image: git.infra.mintel.me/mmintel/gatekeeper:${IMAGE_TAG:-latest} restart: always networks: - infra @@ -53,7 +53,7 @@ services: - "traefik.http.services.${PROJECT_NAME}-gatekeeper.loadbalancer.server.port=3000" directus: - image: registry.infra.mintel.me/mintel/directus:${IMAGE_TAG:-latest} + image: git.infra.mintel.me/mmintel/directus:${IMAGE_TAG:-latest} restart: always networks: - infra diff --git a/packages/infra/gitea/deploy-action.yml b/packages/infra/gitea/deploy-action.yml index 8a592a2..e080747 100644 --- a/packages/infra/gitea/deploy-action.yml +++ b/packages/infra/gitea/deploy-action.yml @@ -180,9 +180,9 @@ jobs: - name: πŸ” Registry Login uses: docker/login-action@v3 with: - registry: registry.infra.mintel.me - username: ${{ secrets.REGISTRY_USER }} - password: ${{ secrets.REGISTRY_PASS }} + registry: git.infra.mintel.me + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: πŸ—οΈ Docker Build & Push uses: docker/build-push-action@v5 @@ -198,7 +198,7 @@ jobs: push: true secrets: | NPM_TOKEN=${{ secrets.NPM_TOKEN }} - tags: registry.infra.mintel.me/mintel/${{ github.event.repository.name }}:${{ needs.prepare.outputs.image_tag }} + tags: git.infra.mintel.me/mmintel/${{ github.event.repository.name }}:${{ needs.prepare.outputs.image_tag }} # ────────────────────────────────────────────────────────────────────────────── # JOB 4: Deploy @@ -262,7 +262,7 @@ jobs: set -e cd "/home/deploy/sites/${{ github.event.repository.name }}" chmod 600 "$ENV_FILE" - echo "${{ secrets.REGISTRY_PASS }}" | docker login registry.infra.mintel.me -u "${{ secrets.REGISTRY_USER }}" --password-stdin + echo "${{ secrets.GITHUB_TOKEN }}" | docker login git.infra.mintel.me -u "${{ github.actor }}" --password-stdin docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" pull docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" up -d --remove-orphans docker system prune -f --filter "until=24h" diff --git a/packages/payload-mcp-chat/README.md b/packages/payload-mcp-chat/README.md new file mode 100644 index 0000000..7ed6ee6 --- /dev/null +++ b/packages/payload-mcp-chat/README.md @@ -0,0 +1,49 @@ +# @mintel/payload-mcp-chat + +A powerful, native AI Chat plugin for Payload CMS v3 with fine-grained Model Context Protocol (MCP) tool execution permissions. + +Unlike generic MCP plugins, this package builds the core tool adapter *inside* Payload via the Local API. This allows Administrators to explicitly dictate exactly which tools, collections, and external MCP servers specific Users or Roles can access. + +## Features + +- **Floating AI Chat Pane:** Exists universally across the Payload Admin Panel. +- **Native Local API Tools:** AI automatically gets tools to read/create/update documents. +- **Strict Role-Based AI Permissions:** A custom `AIChatPermissions` collection controls what the AI is allowed to execute on behalf of the current logged-in user. +- **Flexible External MCP Support:** Connect standard external MCP servers (via HTTP or STDIO) and seamlessly make their tools available to the Chat window, all wrapped within the permission engine. +- **Vercel AI SDK Integration:** Powered by the robust `ai` package using reliable streaming protocols. + +## Installation + +```bash +pnpm add @mintel/payload-mcp-chat @modelcontextprotocol/sdk ai +``` + +## Setup + +Wrap your payload config with the plugin: + +```typescript +// payload.config.ts +import { buildConfig } from 'payload' +import { payloadMcpChatPlugin } from '@mintel/payload-mcp-chat' + +export default buildConfig({ + // ... your config + plugins: [ + payloadMcpChatPlugin({ + enabled: true, + // optional setup config here + }) + ] +}) +``` + +## Permissions Model + +The plugin automatically registers a Global (or Collection depending on setup) called **AI Chat Permissions**. +Here, an Admin can: +1. Select a `User` or define a `Role`. +2. Select which Payload Collections they are allowed to manage via AI. +3. Select which registered external MCP Servers they are allowed to use. + +If a user asks the AI to update a user's password, and the `users` collection is not checked in their AI Chat Permission config, the AI will not even receive the tool to perform the action. If it hallucinates the tool, the backend will strictly block it. diff --git a/packages/payload-mcp-chat/package.json b/packages/payload-mcp-chat/package.json new file mode 100644 index 0000000..e0afcce --- /dev/null +++ b/packages/payload-mcp-chat/package.json @@ -0,0 +1,48 @@ +{ + "name": "@mintel/payload-mcp-chat", + "version": "1.0.0", + "private": true, + "description": "Payload CMS Plugin for MCP AI Chat with custom permissions", + "type": "module", + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": "./dist/index.js", + "./components/*": "./dist/components/*", + "./actions/*": "./dist/actions/*", + "./endpoints/*": "./dist/endpoints/*", + "./tools/*": "./dist/tools/*", + "./utils/*": "./dist/utils/*" + }, + "peerDependencies": { + "@payloadcms/next": ">=3.0.0", + "@payloadcms/ui": ">=3.0.0", + "payload": ">=3.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + }, + "dependencies": { + "@ai-sdk/openai": "^3.0.39", + "@modelcontextprotocol/sdk": "^1.6.0", + "@qdrant/js-client-rest": "^1.17.0", + "ai": "^4.1.41", + "lucide-react": "^0.475.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@payloadcms/next": "3.77.0", + "@payloadcms/ui": "3.77.0", + "@types/node": "^20.17.17", + "@types/react": "^19.2.8", + "@types/react-dom": "^19.2.3", + "next": "^15.1.0", + "payload": "3.77.0", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "typescript": "^5.7.3" + } +} \ No newline at end of file diff --git a/packages/payload-mcp-chat/src/collections/AIChatPermissions.ts b/packages/payload-mcp-chat/src/collections/AIChatPermissions.ts new file mode 100644 index 0000000..2aeaa25 --- /dev/null +++ b/packages/payload-mcp-chat/src/collections/AIChatPermissions.ts @@ -0,0 +1,69 @@ +import type { CollectionConfig } from 'payload' + +/** + * A central collection to manage which AI Tools/MCPs a User or Role is allowed to use. + */ +export const AIChatPermissionsCollection: CollectionConfig = { + slug: 'ai-chat-permissions', + labels: { + singular: 'AI Chat Permission', + plural: 'AI Chat Permissions', + }, + admin: { + useAsTitle: 'description', + group: 'AI & Tools', + }, + fields: [ + { + name: 'description', + type: 'text', + required: true, + admin: { + description: 'E.g. "Editors default AI permissions"', + }, + }, + { + type: 'row', + fields: [ + { + name: 'targetUser', + type: 'relationship', + relationTo: 'users', + hasMany: false, + admin: { + description: 'Apply these permissions to a specific user (optional).', + }, + }, + { + name: 'targetRole', + type: 'select', + options: [ + { label: 'Admin', value: 'admin' }, + { label: 'Editor', value: 'editor' }, + ], // Ideally this is dynamically populated in a real scenario, but we hardcode standard roles for now + admin: { + description: 'Apply these permissions to all users with this role.', + }, + }, + ], + }, + { + name: 'allowedCollections', + type: 'select', + hasMany: true, + options: [], // Will be populated dynamically in the plugin init based on actual collections + admin: { + description: 'Which Payload collections is the AI allowed to read/write on behalf of this user?', + }, + }, + { + name: 'allowedMcpServers', + type: 'select', + hasMany: true, + options: [], // Will be populated dynamically based on plugin config + admin: { + description: 'Which external MCP Servers is the AI allowed to execute tools from?', + }, + } + ], +} diff --git a/packages/payload-mcp-chat/src/components/ChatWindow/index.tsx b/packages/payload-mcp-chat/src/components/ChatWindow/index.tsx new file mode 100644 index 0000000..c44bd33 --- /dev/null +++ b/packages/payload-mcp-chat/src/components/ChatWindow/index.tsx @@ -0,0 +1,106 @@ +'use client' + +import React, { useState } from 'react' +import { useChat } from 'ai/react' +import './ChatWindow.scss' + +export const ChatWindowProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + <> + {children} + + + ) +} + +const ChatWindow: React.FC = () => { + const [isOpen, setIsOpen] = useState(false) + const { messages, input, handleInputChange, handleSubmit, setMessages } = useChat({ + api: '/api/mcp-chat', + }) + + // Basic implementation to toggle chat window and submit messages + return ( +
+ + + {isOpen && ( +
+
+

Payload MCP Chat

+
+ +
+ {messages.map(m => ( +
+
+ {m.role === 'user' ? 'G: ' : 'AI: '} + {m.content} +
+
+ ))} +
+ +
+ +
+
+ )} +
+ ) +} diff --git a/packages/payload-mcp-chat/src/endpoints/chatEndpoint.ts b/packages/payload-mcp-chat/src/endpoints/chatEndpoint.ts new file mode 100644 index 0000000..2146605 --- /dev/null +++ b/packages/payload-mcp-chat/src/endpoints/chatEndpoint.ts @@ -0,0 +1,75 @@ +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', +}) + +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 }) + } + + const { messages } = await req.json() + + // 1. Check AI Permissions for req.user + // In a real implementation this looks up the global or collection for permissions + const allowedCollections = ['users'] // Stub + let activeTools: Record = {} + + // 2. Generate Payload Local Tools + if (allowedCollections.length > 0) { + const payloadTools = generatePayloadLocalTools(req.payload, req, allowedCollections) + activeTools = { ...activeTools, ...payloadTools } + } + + // 3. Connect External MCPs + const allowedMcpServers: string[] = [] // Stub + if (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 = ` + 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. + ` + + try { + const result = streamText({ + // @ts-ignore - AI SDK type mismatch + model: openrouter('google/gemini-3.0-flash'), + messages, + tools: activeTools, + system: `You are a helpful Payload CMS MCP Assistant orchestrating the local Mintel ecosystem. + You only have access to tools explicitly granted by the Admin. + You cannot do anything outside these tools. Always explain what you are doing. + ${memorySystemPrompt}` + }) + + return result.toDataStreamResponse() + } catch (error) { + console.error("AI Error:", error) + return Response.json({ error: 'Failed to process AI request' }, { status: 500 }) + } +} diff --git a/packages/payload-mcp-chat/src/index.ts b/packages/payload-mcp-chat/src/index.ts new file mode 100644 index 0000000..286eed4 --- /dev/null +++ b/packages/payload-mcp-chat/src/index.ts @@ -0,0 +1,2 @@ +export { payloadMcpChatPlugin } from './plugin.js' +export type { PayloadMcpChatPluginConfig } from './types.js' diff --git a/packages/payload-mcp-chat/src/plugin.ts b/packages/payload-mcp-chat/src/plugin.ts new file mode 100644 index 0000000..eac495c --- /dev/null +++ b/packages/payload-mcp-chat/src/plugin.ts @@ -0,0 +1,68 @@ +import type { Config, Plugin } from 'payload' +import { AIChatPermissionsCollection } from './collections/AIChatPermissions.js' +import type { PayloadMcpChatPluginConfig } from './types.js' + +export const payloadMcpChatPlugin = + (pluginOptions: PayloadMcpChatPluginConfig): Plugin => + (incomingConfig) => { + let config = { ...incomingConfig } + + // If disabled, return config untouched + if (pluginOptions.enabled === false) { + return config + } + + // 1. Inject the Permissions Collection into the Schema + const existingCollections = config.collections || [] + + 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 + })) + } + + 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] + + // 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" }) + }, + }, + ] + + // 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-mcp-chat/components/ChatWindow#ChatWindowProvider', + ], + }, + } + } + + return config + } diff --git a/packages/payload-mcp-chat/src/tools/mcpAdapter.ts b/packages/payload-mcp-chat/src/tools/mcpAdapter.ts new file mode 100644 index 0000000..ac3b6e0 --- /dev/null +++ b/packages/payload-mcp-chat/src/tools/mcpAdapter.ts @@ -0,0 +1,64 @@ +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 + + // 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: {} } + ) + + await client.connect(transport) + + // Fetch available tools from the external MCP server + const toolListResult = await client.listTools() + const externalTools = toolListResult.tools || [] + + const aiSdkTools: Record = {} + + // 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 + + // 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 + execute: async (args: any) => { + const result = await client.callTool({ + name: extTool.name, + arguments: args + }) + return result + } + }) + } + + return { tools: aiSdkTools, client } +} diff --git a/packages/payload-mcp-chat/src/tools/memoryDb.ts b/packages/payload-mcp-chat/src/tools/memoryDb.ts new file mode 100644 index 0000000..9368baa --- /dev/null +++ b/packages/payload-mcp-chat/src/tools/memoryDb.ts @@ -0,0 +1,113 @@ +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, +}) + +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) => 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() + +/** + * Returns memory tools for the AI SDK. + * Note: A real implementation would require an embedding step before inserting into Qdrant. + * For this implementation, we use a placeholder or assume the embeddings are handled + * 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".'), + }), + execute: async ({ fact, category }) => { + // 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()) + + 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.'), + }), + execute: async ({ query }) => { + // 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 => r.payload?.fact || '') + } catch (error) { + console.error("Qdrant search error:", error) + return [] + } + } + }) + } +} diff --git a/packages/payload-mcp-chat/src/tools/payloadLocal.ts b/packages/payload-mcp-chat/src/tools/payloadLocal.ts new file mode 100644 index 0000000..0b24826 --- /dev/null +++ b/packages/payload-mcp-chat/src/tools/payloadLocal.ts @@ -0,0 +1,102 @@ +import { tool } from 'ai' +import { z } from 'zod' +import type { Payload, PayloadRequest, User } from 'payload' + +export const generatePayloadLocalTools = ( + payload: Payload, + req: PayloadRequest, + allowedCollections: string[] +) => { + const tools: Record = {} + + 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.'), + }), + execute: async ({ limit = 10, page = 1, query }) => { + 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! + }) + }, + }) + + // 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.'), + }), + execute: async ({ id }) => { + 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.'), + }), + execute: async ({ data }) => { + 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.'), + }), + execute: async ({ id, data }) => { + 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.'), + }), + execute: async ({ id }) => { + return await payload.delete({ + collection: collectionSlug as any, + id, + req, // Enforce access control + }) + }, + }) + } + + return tools +} diff --git a/packages/payload-mcp-chat/src/types.ts b/packages/payload-mcp-chat/src/types.ts new file mode 100644 index 0000000..7319988 --- /dev/null +++ b/packages/payload-mcp-chat/src/types.ts @@ -0,0 +1,18 @@ +import type { Plugin } from 'payload' + +export interface PayloadMcpChatPluginConfig { + enabled?: boolean + /** + * Defines whether to render the floating chat bubble in the admin panel automatically. + * Defaults to true. + */ + renderChatBubble?: boolean + /** + * Used to register external MCP servers that the AI can explicitly connect to if the admin permits it. + */ + mcpServers?: { + name: string + url?: string + // Command based STDIO later via configuration + }[] +} diff --git a/packages/payload-mcp-chat/tsconfig.json b/packages/payload-mcp-chat/tsconfig.json new file mode 100644 index 0000000..75ba716 --- /dev/null +++ b/packages/payload-mcp-chat/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "jsx": "preserve", + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "declarationDir": "dist", + "skipLibCheck": true, + "lib": [ + "es2022", + "DOM", + "DOM.Iterable" + ] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7658fb4..88b2d78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -232,7 +232,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) packages/content-engine: dependencies: @@ -472,7 +472,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) packages/meme-generator: dependencies: @@ -647,7 +647,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) + version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) packages/page-audit: dependencies: @@ -717,6 +717,58 @@ importers: specifier: ^5.7.3 version: 5.9.3 + packages/payload-mcp-chat: + dependencies: + '@ai-sdk/openai': + specifier: ^3.0.39 + version: 3.0.39(zod@3.25.76) + '@modelcontextprotocol/sdk': + specifier: ^1.6.0 + version: 1.27.1(zod@3.25.76) + '@qdrant/js-client-rest': + specifier: ^1.17.0 + version: 1.17.0(typescript@5.9.3) + ai: + specifier: ^4.1.41 + version: 4.3.19(react@19.2.4)(zod@3.25.76) + lucide-react: + specifier: ^0.475.0 + version: 0.475.0(react@19.2.4) + zod: + specifier: ^3.25.76 + version: 3.25.76 + devDependencies: + '@payloadcms/next': + specifier: 3.77.0 + version: 3.77.0(@types/react@19.2.13)(graphql@16.13.0)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.13.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + '@payloadcms/ui': + specifier: 3.77.0 + version: 3.77.0(@types/react@19.2.13)(monaco-editor@0.55.1)(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(payload@3.77.0(graphql@16.13.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + '@types/node': + specifier: ^20.17.17 + version: 20.19.33 + '@types/react': + specifier: ^19.2.8 + version: 19.2.13 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.13) + next: + specifier: 16.1.6 + version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) + payload: + specifier: 3.77.0 + version: 3.77.0(graphql@16.13.0)(typescript@5.9.3) + react: + specifier: ^19.2.3 + version: 19.2.4 + react-dom: + specifier: ^19.2.3 + version: 19.2.4(react@19.2.4) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + packages/pdf-library: dependencies: '@crawlee/cheerio': @@ -774,7 +826,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0) packages/thumbnail-generator: dependencies: @@ -808,6 +860,48 @@ packages: '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + '@ai-sdk/openai@3.0.39': + resolution: {integrity: sha512-EZrs4L6kMkPQhpodagpEvqLSryOIK99WgblN0IsVHr1xhajWizQOZ0XMa7c5JpSYgIjV6u8GCpGV6hS3Mk2Bug==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@2.2.8': + resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/provider-utils@4.0.17': + resolution: {integrity: sha512-oyCeFINTYK0B8ZGUBiQc05G5vytPlKSmTTtm19xfJuUgoi8zkvvRcoPQci4mSnyfpPn2XSFFDfsALG8uGcapfg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@1.1.3': + resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} + engines: {node: '>=18'} + + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} + engines: {node: '>=18'} + + '@ai-sdk/react@1.2.12': + resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + + '@ai-sdk/ui-utils@1.2.11': + resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -2482,6 +2576,16 @@ packages: peerDependencies: '@opentelemetry/api': ^1.8 + '@qdrant/js-client-rest@1.17.0': + resolution: {integrity: sha512-aZFQeirWVqWAa1a8vJ957LMzcXkFHGbsoRhzc8AkGfg6V0jtK8PlG8/eyyc2xhYsR961FDDx1Tx6nyE0K7lS+A==} + engines: {node: '>=18.17.0', pnpm: '>=8'} + peerDependencies: + typescript: '>=4.7' + + '@qdrant/openapi-typescript-fetch@1.2.6': + resolution: {integrity: sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==} + engines: {node: '>=18.0.0', pnpm: '>=8'} + '@react-email/body@0.0.11': resolution: {integrity: sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==} peerDependencies: @@ -3115,6 +3219,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/diff-match-patch@1.0.36': + resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -3564,6 +3671,16 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} + ai@4.3.19: + resolution: {integrity: sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + react: + optional: true + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -4295,6 +4412,9 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -5512,6 +5632,9 @@ packages: json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -5524,6 +5647,11 @@ packages: engines: {node: '>=6'} hasBin: true + jsondiffpatch@0.6.0: + resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -5739,6 +5867,11 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lucide-react@0.475.0: + resolution: {integrity: sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -6761,6 +6894,9 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} @@ -7087,6 +7223,11 @@ packages: svg-arc-to-cubic-bezier@3.2.0: resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==} + swr@2.4.1: + resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -7154,6 +7295,10 @@ packages: resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} engines: {node: '>=20'} + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -7383,6 +7528,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@6.23.0: + resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} + engines: {node: '>=18.17'} + undici@7.18.2: resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} engines: {node: '>=20.18.1'} @@ -7440,6 +7589,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utf8-byte-length@1.0.5: resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} @@ -7839,6 +7993,51 @@ snapshots: '@adobe/css-tools@4.4.4': {} + '@ai-sdk/openai@3.0.39(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.17(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/provider-utils@2.2.8(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + nanoid: 3.3.11 + secure-json-parse: 2.7.0 + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.17(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + + '@ai-sdk/provider@1.1.3': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/provider@3.0.8': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@1.2.12(react@19.2.4)(zod@3.25.76)': + dependencies: + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) + react: 19.2.4 + swr: 2.4.1(react@19.2.4) + throttleit: 2.1.0 + optionalDependencies: + zod: 3.25.76 + + '@ai-sdk/ui-utils@1.2.11(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + '@alloc/quick-lru@5.2.0': {} '@apidevtools/json-schema-ref-parser@11.9.3': @@ -9691,6 +9890,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@qdrant/js-client-rest@1.17.0(typescript@5.9.3)': + dependencies: + '@qdrant/openapi-typescript-fetch': 1.2.6 + typescript: 5.9.3 + undici: 6.23.0 + + '@qdrant/openapi-typescript-fetch@1.2.6': {} + '@react-email/body@0.0.11(react@19.2.4)': dependencies: react: 19.2.4 @@ -10418,6 +10625,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/diff-match-patch@1.0.36': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -10951,6 +11160,18 @@ snapshots: dependencies: humanize-ms: 1.2.1 + ai@4.3.19(react@19.2.4)(zod@3.25.76): + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@ai-sdk/react': 1.2.12(react@19.2.4)(zod@3.25.76) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + jsondiffpatch: 0.6.0 + zod: 3.25.76 + optionalDependencies: + react: 19.2.4 + ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -11674,6 +11895,8 @@ snapshots: didyoumean@1.2.2: {} + diff-match-patch@1.0.5: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -13221,6 +13444,8 @@ snapshots: json-schema-typed@8.0.2: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -13229,6 +13454,12 @@ snapshots: json5@2.2.3: {} + jsondiffpatch@0.6.0: + dependencies: + '@types/diff-match-patch': 1.0.36 + chalk: 5.6.2 + diff-match-patch: 1.0.5 + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -13424,6 +13655,10 @@ snapshots: dependencies: react: 19.2.4 + lucide-react@0.475.0(react@19.2.4): + dependencies: + react: 19.2.4 + lz-string@1.5.0: {} magic-string@0.30.21: @@ -14532,6 +14767,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) + secure-json-parse@2.7.0: {} + secure-json-parse@4.1.0: {} selderee@0.11.0: @@ -14930,6 +15167,12 @@ snapshots: svg-arc-to-cubic-bezier@3.2.0: {} + swr@2.4.1(react@19.2.4): + dependencies: + dequal: 2.0.3 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + symbol-tree@3.2.4: {} tabbable@6.4.0: {} @@ -15032,6 +15275,8 @@ snapshots: dependencies: real-require: 0.2.0 + throttleit@2.1.0: {} + through@2.3.8: {} tiny-inflate@1.0.3: {} @@ -15257,6 +15502,8 @@ snapshots: undici-types@6.21.0: {} + undici@6.23.0: {} + undici@7.18.2: {} unicode-properties@1.4.1: @@ -15335,6 +15582,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.13 + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + utf8-byte-length@1.0.5: {} util-deprecate@1.0.2: {} @@ -15453,7 +15704,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitest@2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): + vitest@2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): dependencies: '@vitest/expect': 2.1.9 '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)) @@ -15491,7 +15742,7 @@ snapshots: - supports-color - terser - vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -15533,7 +15784,7 @@ snapshots: - supports-color - terser - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4