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

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

View File

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

View File

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