chore: sync lockfile and payload-ai extensions for release v1.9.10
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m20s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m27s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m35s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Failing after 17s
Monorepo Pipeline / 🐳 Build Build-Base (push) Failing after 17s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Failing after 17s
Monorepo Pipeline / 🚀 Release (push) Successful in 1m33s

This commit is contained in:
2026-03-03 12:40:41 +01:00
parent 24fde20030
commit 79d221de5e
22 changed files with 838 additions and 325 deletions

View File

@@ -0,0 +1,65 @@
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<string, any> = {}
// Map each external tool to a Vercel AI SDK Tool
for (const extTool of externalTools) {
// Basic conversion of JSON Schema to Zod for the AI SDK
// Note: For a production ready adapter, you might need a more robust jsonSchemaToZod converter
// or use AI SDK's new experimental generateSchema feature if available.
// Here we use a generic `z.any()` as a fallback since AI SDK requires a Zod schema.
const toolSchema = extTool.inputSchema as Record<string, any>
// We create a simplified parameter parser.
// An ideal approach uses `jsonSchemaToZod` library or native AI SDK JSON schema support
// (introduced recently in `ai` package).
aiSdkTools[`${mcpConfig.name}_${extTool.name}`] = tool({
description: `[From ${mcpConfig.name}] ${extTool.description || extTool.name}`,
parameters: z.any().describe('JSON matching the original MCP input_schema'), // Simplify for prototype
// @ts-ignore - AI strict mode overload bug with implicit zod inferences
execute: async (args: any) => {
const result = await client.callTool({
name: extTool.name,
arguments: args
})
return result
}
})
}
return { tools: aiSdkTools, client }
}

View File

@@ -0,0 +1,115 @@
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: any) => c.name === MEMORY_COLLECTION)
if (!exists) {
await qdrantClient.createCollection(MEMORY_COLLECTION, {
vectors: {
size: 1536, // typical embedding size, adjust based on the embedding model used
distance: 'Cosine',
},
})
console.log(`Qdrant collection '${MEMORY_COLLECTION}' created.`)
}
} catch (error) {
console.error('Failed to initialize Qdrant memory collection:', error)
}
}
// Call init, but don't block
initQdrant()
/**
* 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".'),
}),
// @ts-ignore - AI SDK strict mode bug
execute: async ({ fact, category }: { fact: string; category?: string }) => {
// In a real scenario, you MUST generate embeddings for the 'fact' string here
// using OpenAI or another embedding provider before inserting into Qdrant.
// const embedding = await generateEmbedding(fact)
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.'),
}),
// @ts-ignore - AI SDK strict mode bug
execute: async ({ query }: { query: string }) => {
// Generate embedding for query
const mockQueryEmbedding = new Array(1536).fill(0).map(() => Math.random())
try {
const results = await qdrantClient.search(MEMORY_COLLECTION, {
vector: mockQueryEmbedding,
limit: 5,
filter: {
must: [
{
key: 'userId',
match: { value: String(userId) }
}
]
}
})
return results.map((r: any) => r.payload?.fact || '')
} catch (error) {
console.error("Qdrant search error:", error)
return []
}
}
})
}
}

View File

@@ -0,0 +1,107 @@
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<string, any> = {}
for (const collectionSlug of allowedCollections) {
const slugKey = collectionSlug.replace(/-/g, '_')
// 1. Read (Find) Tool
tools[`read_${slugKey}`] = tool({
description: `Read/Find documents from the Payload CMS collection: ${collectionSlug}`,
parameters: z.object({
limit: z.number().optional().describe('Number of documents to return, max 100.'),
page: z.number().optional().describe('Page number for pagination.'),
// Simple string-based query for demo purposes. For a robust implementation,
// we'd map this to Payload's where query logic using a structured Zod schema.
query: z.string().optional().describe('Optional text to search within the collection.'),
}),
// @ts-ignore - AI SDK strict mode type inference bug
execute: async ({ limit = 10, page = 1, query }: { limit?: number; page?: number; query?: string }) => {
const where = query ? { id: { equals: query } } : undefined // Placeholder logic
return await payload.find({
collection: collectionSlug as any,
limit: Math.min(limit, 100),
page,
where,
req, // Crucial for passing the user context and respecting access control!
})
},
})
// 2. Read by ID Tool
tools[`read_${slugKey}_by_id`] = tool({
description: `Get a specific document by its ID from the ${collectionSlug} collection.`,
parameters: z.object({
id: z.union([z.string(), z.number()]).describe('The ID of the document.'),
}),
// @ts-ignore - AI SDK strict mode type inference bug
execute: async ({ id }: { id: string | number }) => {
return await payload.findByID({
collection: collectionSlug as any,
id,
req, // Enforce access control
})
},
})
// 3. Create Tool
tools[`create_${slugKey}`] = tool({
description: `Create a new document in the ${collectionSlug} collection.`,
parameters: z.object({
data: z.record(z.any()).describe('A JSON object containing the data to insert.'),
}),
// @ts-ignore - AI SDK strict mode type inference bug
execute: async ({ data }: { data: Record<string, any> }) => {
return await payload.create({
collection: collectionSlug as any,
data,
req, // Enforce access control
})
},
})
// 4. Update Tool
tools[`update_${slugKey}`] = tool({
description: `Update an existing document in the ${collectionSlug} collection.`,
parameters: z.object({
id: z.union([z.string(), z.number()]).describe('The ID of the document to update.'),
data: z.record(z.any()).describe('A JSON object containing the fields to update.'),
}),
// @ts-ignore - AI SDK strict mode type inference bug
execute: async ({ id, data }: { id: string | number; data: Record<string, any> }) => {
return await payload.update({
collection: collectionSlug as any,
id,
data,
req, // Enforce access control
})
},
})
// 5. Delete Tool
tools[`delete_${slugKey}`] = tool({
description: `Delete a document from the ${collectionSlug} collection by ID.`,
parameters: z.object({
id: z.union([z.string(), z.number()]).describe('The ID of the document to delete.'),
}),
// @ts-ignore - AI SDK strict mode type inference bug
execute: async ({ id }: { id: string | number }) => {
return await payload.delete({
collection: collectionSlug as any,
id,
req, // Enforce access control
})
},
})
}
return tools
}