Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1670b8e5ef | |||
| 1c43d12e4d | |||
| 5cf9922822 | |||
| 9a4a95feea | |||
| d3902c4c77 | |||
| 21ec8a33ae | |||
| 79d221de5e | |||
| 24fde20030 | |||
| 4a4409ca85 |
@@ -1,5 +1,5 @@
|
|||||||
# Project
|
# Project
|
||||||
IMAGE_TAG=v1.9.9
|
IMAGE_TAG=v1.9.10
|
||||||
PROJECT_NAME=sample-website
|
PROJECT_NAME=sample-website
|
||||||
PROJECT_COLOR=#82ed20
|
PROJECT_COLOR=#82ed20
|
||||||
|
|
||||||
|
|||||||
@@ -203,8 +203,8 @@ jobs:
|
|||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: git.infra.mintel.me
|
registry: git.infra.mintel.me
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
- name: 🏗️ Build & Push ${{ matrix.name }}
|
- name: 🏗️ Build & Push ${{ matrix.name }}
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -46,4 +46,8 @@ directus/uploads/directus-health-file
|
|||||||
# Estimation Engine Data
|
# Estimation Engine Data
|
||||||
data/crawls/
|
data/crawls/
|
||||||
packages/estimation-engine/out/
|
packages/estimation-engine/out/
|
||||||
apps/web/out/estimations/
|
apps/web/out/estimations/
|
||||||
|
|
||||||
|
# Memory MCP
|
||||||
|
data/qdrant/
|
||||||
|
packages/memory-mcp/models/
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sample-website",
|
"name": "sample-website",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
16
docker-compose.mcps.yml
Normal file
16
docker-compose.mcps.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
services:
|
||||||
|
qdrant:
|
||||||
|
image: qdrant/qdrant:latest
|
||||||
|
container_name: qdrant-mcp
|
||||||
|
ports:
|
||||||
|
- "6333:6333"
|
||||||
|
- "6334:6334"
|
||||||
|
volumes:
|
||||||
|
- ./data/qdrant:/qdrant/storage
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- mcp-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
mcp-network:
|
||||||
|
driver: bridge
|
||||||
12
fix-private.mjs
Normal file
12
fix-private.mjs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import glob from 'glob';
|
||||||
|
|
||||||
|
const files = glob.sync('/Users/marcmintel/Projects/at-mintel/packages/*/package.json');
|
||||||
|
files.forEach(f => {
|
||||||
|
const content = fs.readFileSync(f, 'utf8');
|
||||||
|
if (content.includes('"private": true,')) {
|
||||||
|
console.log(`Fixing ${f}`);
|
||||||
|
const newContent = content.replace(/\s*"private": true,?\n/g, '\n');
|
||||||
|
fs.writeFileSync(f, newContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"dev:gatekeeper": "bash -c 'trap \"COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml up --build --remove-orphans'",
|
"dev:gatekeeper": "bash -c 'trap \"COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml down && COMPOSE_PROJECT_NAME=gatekeeper docker-compose -f docker-compose.gatekeeper.yml up --build --remove-orphans'",
|
||||||
"dev:mcps:up": "docker-compose -f docker-compose.mcps.yml up -d",
|
"dev:mcps:up": "docker-compose -f docker-compose.mcps.yml up -d",
|
||||||
"dev:mcps:down": "docker-compose -f docker-compose.mcps.yml down",
|
"dev:mcps:down": "docker-compose -f docker-compose.mcps.yml down",
|
||||||
"dev:mcps:watch": "pnpm -r --filter=\"./packages/*-mcp\" run dev",
|
"dev:mcps:watch": "pnpm -r --filter=\"./packages/*-mcp\" exec tsc -w",
|
||||||
"dev:mcps": "npm run dev:mcps:up && npm run dev:mcps:watch",
|
"dev:mcps": "npm run dev:mcps:up && npm run dev:mcps:watch",
|
||||||
"lint": "pnpm -r --filter='./packages/**' --filter='./apps/**' lint",
|
"lint": "pnpm -r --filter='./packages/**' --filter='./apps/**' lint",
|
||||||
"test": "pnpm -r test",
|
"test": "pnpm -r test",
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"require-in-the-middle": "^8.0.1"
|
"require-in-the-middle": "^8.0.1"
|
||||||
},
|
},
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/cli",
|
"name": "@mintel/cli",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/cloner",
|
"name": "@mintel/cloner",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/concept-engine",
|
"name": "@mintel/concept-engine",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"private": true,
|
|
||||||
"description": "AI-powered web project concept generation and analysis",
|
"description": "AI-powered web project concept generation and analysis",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/content-engine",
|
"name": "@mintel/content-engine",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"private": false,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/eslint-config",
|
"name": "@mintel/eslint-config",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/estimation-engine",
|
"name": "@mintel/estimation-engine",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/gatekeeper",
|
"name": "@mintel/gatekeeper",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/gitea-mcp",
|
"name": "@mintel/gitea-mcp",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"description": "Native Gitea MCP server for 100% Antigravity compatibility",
|
"description": "Native Gitea MCP server for 100% Antigravity compatibility",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/husky-config",
|
"name": "@mintel/husky-config",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
||||||
|
|||||||
@@ -181,8 +181,8 @@ jobs:
|
|||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: git.infra.mintel.me
|
registry: git.infra.mintel.me
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
- name: 🏗️ Docker Build & Push
|
- name: 🏗️ Docker Build & Push
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
@@ -262,7 +262,7 @@ jobs:
|
|||||||
set -e
|
set -e
|
||||||
cd "/home/deploy/sites/${{ github.event.repository.name }}"
|
cd "/home/deploy/sites/${{ github.event.repository.name }}"
|
||||||
chmod 600 "$ENV_FILE"
|
chmod 600 "$ENV_FILE"
|
||||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login git.infra.mintel.me -u "${{ github.actor }}" --password-stdin
|
echo "${{ secrets.NPM_TOKEN }}" | docker login git.infra.mintel.me -u "${{ github.repository_owner }}" --password-stdin
|
||||||
docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" pull
|
docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" pull
|
||||||
docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" up -d --remove-orphans
|
docker compose -p "$PROJECT_NAME" --env-file "$ENV_FILE" up -d --remove-orphans
|
||||||
docker system prune -f --filter "until=24h"
|
docker system prune -f --filter "until=24h"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/infra",
|
"name": "@mintel/infra",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/journaling",
|
"name": "@mintel/journaling",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/mail",
|
"name": "@mintel/mail",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"private": false,
|
"private": false,
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/meme-generator",
|
"name": "@mintel/meme-generator",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"private": false,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/memory-mcp",
|
"name": "@mintel/memory-mcp",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"description": "Local Qdrant-based Memory MCP server",
|
"description": "Local Qdrant-based Memory MCP server",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
78
packages/memory-mcp/src/index.ts
Normal file
78
packages/memory-mcp/src/index.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { QdrantMemoryService } from './qdrant.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const server = new McpServer({
|
||||||
|
name: '@mintel/memory-mcp',
|
||||||
|
version: '1.0.0',
|
||||||
|
});
|
||||||
|
|
||||||
|
const qdrantService = new QdrantMemoryService(process.env.QDRANT_URL || 'http://localhost:6333');
|
||||||
|
|
||||||
|
// Initialize embedding model and Qdrant connection
|
||||||
|
try {
|
||||||
|
await qdrantService.initialize();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to initialize local dependencies. Exiting.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
'store_memory',
|
||||||
|
'Store a new piece of knowledge/memory into the vector database. Use this to remember architectural decisions, preferences, aliases, etc.',
|
||||||
|
{
|
||||||
|
label: z.string().describe('A short, descriptive label or title for the memory (e.g., "Architektur-Entscheidungen")'),
|
||||||
|
content: z.string().describe('The actual content to remember (e.g., "In diesem Projekt nutzen wir lieber Composition over Inheritance.")'),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
const success = await qdrantService.storeMemory(args.label, args.content);
|
||||||
|
if (success) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Successfully stored memory: [${args.label}]` }],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Failed to store memory: [${args.label}]` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
'retrieve_memory',
|
||||||
|
'Retrieve relevant memories from the vector database based on a semantic search query.',
|
||||||
|
{
|
||||||
|
query: z.string().describe('The search query to find relevant memories.'),
|
||||||
|
limit: z.number().optional().describe('Maximum number of results to return (default: 5)'),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
const results = await qdrantService.retrieveMemory(args.query, args.limit || 5);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: 'No relevant memories found.' }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedResults = results
|
||||||
|
.map(r => `- [${r.label}] (Score: ${r.score.toFixed(3)}): ${r.content}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Found ${results.length} memories:\n\n${formattedResults}` }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
console.error('Memory MCP server is running and ready to accept connections over stdio.');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('Fatal error in main():', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
89
packages/memory-mcp/src/qdrant.test.ts
Normal file
89
packages/memory-mcp/src/qdrant.test.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { QdrantMemoryService } from './qdrant.js';
|
||||||
|
|
||||||
|
vi.mock('@xenova/transformers', () => {
|
||||||
|
return {
|
||||||
|
env: { allowRemoteModels: false, localModelPath: './models' },
|
||||||
|
pipeline: vi.fn().mockResolvedValue(async (text: string) => {
|
||||||
|
// Mock embedding generation: returns an array of 384 numbers
|
||||||
|
return { data: new Float32Array(384).fill(0.1) };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockCreateCollection = vi.fn();
|
||||||
|
const mockGetCollections = vi.fn().mockResolvedValue({ collections: [] });
|
||||||
|
const mockUpsert = vi.fn();
|
||||||
|
const mockSearch = vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'test-id',
|
||||||
|
version: 1,
|
||||||
|
score: 0.9,
|
||||||
|
payload: { label: 'Test Label', content: 'Test Content' }
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
vi.mock('@qdrant/js-client-rest', () => {
|
||||||
|
return {
|
||||||
|
QdrantClient: vi.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
getCollections: mockGetCollections,
|
||||||
|
createCollection: mockCreateCollection,
|
||||||
|
upsert: mockUpsert,
|
||||||
|
search: mockSearch
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('QdrantMemoryService', () => {
|
||||||
|
let service: QdrantMemoryService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
service = new QdrantMemoryService('http://localhost:6333');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize and create collection if missing', async () => {
|
||||||
|
mockGetCollections.mockResolvedValueOnce({ collections: [] });
|
||||||
|
await service.initialize();
|
||||||
|
|
||||||
|
expect(mockGetCollections).toHaveBeenCalled();
|
||||||
|
expect(mockCreateCollection).toHaveBeenCalledWith('mcp_memory', expect.any(Object));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not create collection if it already exists', async () => {
|
||||||
|
mockGetCollections.mockResolvedValueOnce({ collections: [{ name: 'mcp_memory' }] });
|
||||||
|
await service.initialize();
|
||||||
|
|
||||||
|
expect(mockCreateCollection).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store memory', async () => {
|
||||||
|
await service.initialize();
|
||||||
|
const result = await service.storeMemory('Design', 'Composition over Inheritance');
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockUpsert).toHaveBeenCalledWith('mcp_memory', expect.objectContaining({
|
||||||
|
wait: true,
|
||||||
|
points: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
payload: expect.objectContaining({
|
||||||
|
label: 'Design',
|
||||||
|
content: 'Composition over Inheritance'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve memory', async () => {
|
||||||
|
await service.initialize();
|
||||||
|
const results = await service.retrieveMemory('Design');
|
||||||
|
|
||||||
|
expect(results).toHaveLength(1);
|
||||||
|
expect(results[0].label).toBe('Test Label');
|
||||||
|
expect(results[0].content).toBe('Test Content');
|
||||||
|
expect(results[0].score).toBe(0.9);
|
||||||
|
});
|
||||||
|
});
|
||||||
110
packages/memory-mcp/src/qdrant.ts
Normal file
110
packages/memory-mcp/src/qdrant.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { pipeline, env } from '@xenova/transformers';
|
||||||
|
import { QdrantClient } from '@qdrant/js-client-rest';
|
||||||
|
|
||||||
|
// Be sure to set local caching options for transformers
|
||||||
|
env.allowRemoteModels = true;
|
||||||
|
env.localModelPath = './models';
|
||||||
|
|
||||||
|
export class QdrantMemoryService {
|
||||||
|
private client: QdrantClient;
|
||||||
|
private collectionName = 'mcp_memory';
|
||||||
|
private embedder: any = null;
|
||||||
|
|
||||||
|
constructor(url: string = 'http://localhost:6333') {
|
||||||
|
this.client = new QdrantClient({ url });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the embedding model and the Qdrant collection
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
// 1. Load the embedding model (using a lightweight model suitable for semantic search)
|
||||||
|
console.error('Loading embedding model...');
|
||||||
|
this.embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
|
||||||
|
|
||||||
|
// 2. Ensure collection exists
|
||||||
|
console.error(`Checking for collection: ${this.collectionName}`);
|
||||||
|
try {
|
||||||
|
const collections = await this.client.getCollections();
|
||||||
|
const exists = collections.collections.some(c => c.name === this.collectionName);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
console.error(`Creating collection: ${this.collectionName}`);
|
||||||
|
await this.client.createCollection(this.collectionName, {
|
||||||
|
vectors: {
|
||||||
|
size: 384, // size for all-MiniLM-L6-v2
|
||||||
|
distance: 'Cosine'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.error('Collection created successfully.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to initialize Qdrant collection:', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a vector embedding for the given text
|
||||||
|
*/
|
||||||
|
private async getEmbedding(text: string): Promise<number[]> {
|
||||||
|
if (!this.embedder) {
|
||||||
|
throw new Error('Embedder not initialized. Call initialize() first.');
|
||||||
|
}
|
||||||
|
const output = await this.embedder(text, { pooling: 'mean', normalize: true });
|
||||||
|
return Array.from(output.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a memory entry into Qdrant
|
||||||
|
*/
|
||||||
|
async storeMemory(label: string, content: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const fullText = `${label}: ${content}`;
|
||||||
|
const vector = await this.getEmbedding(fullText);
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
|
||||||
|
await this.client.upsert(this.collectionName, {
|
||||||
|
wait: true,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
vector,
|
||||||
|
payload: {
|
||||||
|
label,
|
||||||
|
content,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to store memory:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves memory entries relevant to the query
|
||||||
|
*/
|
||||||
|
async retrieveMemory(query: string, limit: number = 5): Promise<Array<{ label: string, content: string, score: number }>> {
|
||||||
|
try {
|
||||||
|
const vector = await this.getEmbedding(query);
|
||||||
|
const searchResults = await this.client.search(this.collectionName, {
|
||||||
|
vector,
|
||||||
|
limit,
|
||||||
|
with_payload: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return searchResults.map(result => ({
|
||||||
|
label: String(result.payload?.label || ''),
|
||||||
|
content: String(result.payload?.content || ''),
|
||||||
|
score: result.score
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to retrieve memory:', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
packages/memory-mcp/tsconfig.json
Normal file
16
packages/memory-mcp/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/next-config",
|
"name": "@mintel/next-config",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/next-feedback",
|
"name": "@mintel/next-feedback",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/next-observability",
|
"name": "@mintel/next-observability",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/next-utils",
|
"name": "@mintel/next-utils",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/observability",
|
"name": "@mintel/observability",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/page-audit",
|
"name": "@mintel/page-audit",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"private": true,
|
|
||||||
"description": "AI-powered website IST-analysis using DataForSEO and Gemini",
|
"description": "AI-powered website IST-analysis using DataForSEO and Gemini",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
2
packages/payload-ai/.npmrc
Normal file
2
packages/payload-ai/.npmrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@mintel:registry=https://git.infra.mintel.me/api/packages/mmintel/npm/
|
||||||
|
//git.infra.mintel.me/api/packages/mmintel/npm/:_authToken=263e7f75d8ada27f3a2e71fd6bd9d95298d48a4d
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/payload-ai",
|
"name": "@mintel/payload-ai",
|
||||||
"version": "1.9.9",
|
"version": "1.9.15",
|
||||||
"private": true,
|
|
||||||
"description": "Reusable Payload CMS AI Extensions",
|
"description": "Reusable Payload CMS AI Extensions",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -27,20 +26,26 @@
|
|||||||
"react-dom": ">=18.0.0"
|
"react-dom": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ai-sdk/openai": "^3.0.39",
|
||||||
|
"@ai-sdk/react": "^3.0.110",
|
||||||
"@mintel/content-engine": "workspace:*",
|
"@mintel/content-engine": "workspace:*",
|
||||||
"@mintel/thumbnail-generator": "workspace:*",
|
"@mintel/thumbnail-generator": "workspace:*",
|
||||||
"replicate": "^1.4.0"
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||||
|
"@qdrant/js-client-rest": "^1.17.0",
|
||||||
|
"ai": "^6.0.108",
|
||||||
|
"replicate": "^1.4.0",
|
||||||
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@payloadcms/next": "3.77.0",
|
"@payloadcms/next": "3.77.0",
|
||||||
"@payloadcms/ui": "3.77.0",
|
"@payloadcms/ui": "3.77.0",
|
||||||
"payload": "3.77.0",
|
|
||||||
"react": "^19.2.3",
|
|
||||||
"react-dom": "^19.2.3",
|
|
||||||
"@types/node": "^20.17.17",
|
"@types/node": "^20.17.17",
|
||||||
"@types/react": "^19.2.8",
|
"@types/react": "^19.2.8",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"next": "^15.1.0",
|
"next": "^15.1.0",
|
||||||
|
"payload": "3.77.0",
|
||||||
|
"react": "^19.2.3",
|
||||||
|
"react-dom": "^19.2.3",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { Config, Plugin } from 'payload'
|
import type { Config, Plugin } from 'payload'
|
||||||
import { AIChatPermissionsCollection } from './collections/AIChatPermissions.js'
|
import { AIChatPermissionsCollection } from './collections/AIChatPermissions.js'
|
||||||
import type { PayloadMcpChatPluginConfig } from './types.js'
|
import type { PayloadChatPluginConfig } from './types.js'
|
||||||
|
import { optimizePostEndpoint } from './endpoints/optimizeEndpoint.js'
|
||||||
|
import { generateSlugEndpoint, generateThumbnailEndpoint, generateSingleFieldEndpoint } from './endpoints/generateEndpoints.js'
|
||||||
|
|
||||||
export const payloadMcpChatPlugin =
|
export const payloadChatPlugin =
|
||||||
(pluginOptions: PayloadMcpChatPluginConfig): Plugin =>
|
(pluginOptions: PayloadChatPluginConfig): Plugin =>
|
||||||
(incomingConfig) => {
|
(incomingConfig) => {
|
||||||
let config = { ...incomingConfig }
|
let config = { ...incomingConfig }
|
||||||
|
|
||||||
@@ -48,6 +50,26 @@ export const payloadMcpChatPlugin =
|
|||||||
return Response.json({ message: "Chat endpoint active" })
|
return Response.json({ message: "Chat endpoint active" })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/api/mintel-ai/optimize',
|
||||||
|
method: 'post',
|
||||||
|
handler: optimizePostEndpoint,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/api/mintel-ai/generate-slug',
|
||||||
|
method: 'post',
|
||||||
|
handler: generateSlugEndpoint,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/api/mintel-ai/generate-thumbnail',
|
||||||
|
method: 'post',
|
||||||
|
handler: generateThumbnailEndpoint,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/api/mintel-ai/generate-single-field',
|
||||||
|
method: 'post',
|
||||||
|
handler: generateSingleFieldEndpoint,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// 3. Inject Chat React Component into Admin UI
|
// 3. Inject Chat React Component into Admin UI
|
||||||
@@ -58,7 +80,7 @@ export const payloadMcpChatPlugin =
|
|||||||
...(config.admin?.components || {}),
|
...(config.admin?.components || {}),
|
||||||
providers: [
|
providers: [
|
||||||
...(config.admin?.components?.providers || []),
|
...(config.admin?.components?.providers || []),
|
||||||
'@mintel/payload-mcp-chat/components/ChatWindow#ChatWindowProvider',
|
'@mintel/payload-ai/components/ChatWindow#ChatWindowProvider',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useChat } from 'ai/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 }) => {
|
||||||
@@ -15,9 +15,11 @@ export const ChatWindowProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||||||
|
|
||||||
const ChatWindow: React.FC = () => {
|
const ChatWindow: React.FC = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
// @ts-ignore - AI hook version mismatch between core and react packages
|
||||||
const { messages, input, handleInputChange, handleSubmit, setMessages } = useChat({
|
const { messages, input, handleInputChange, handleSubmit, setMessages } = useChat({
|
||||||
api: '/api/mcp-chat',
|
api: '/api/mcp-chat',
|
||||||
})
|
initialMessages: []
|
||||||
|
} as any)
|
||||||
|
|
||||||
// Basic implementation to toggle chat window and submit messages
|
// Basic implementation to toggle chat window and submit messages
|
||||||
return (
|
return (
|
||||||
@@ -65,7 +67,7 @@ const ChatWindow: React.FC = () => {
|
|||||||
</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 => (
|
{messages.map((m: any) => (
|
||||||
<div key={m.id} style={{
|
<div key={m.id} style={{
|
||||||
marginBottom: '12px',
|
marginBottom: '12px',
|
||||||
textAlign: m.role === 'user' ? 'right' : 'left'
|
textAlign: m.role === 'user' ? 'right' : 'left'
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useField, useDocumentInfo, useForm } from "@payloadcms/ui";
|
import { useField, useDocumentInfo, useForm } from "@payloadcms/ui";
|
||||||
import { generateSingleFieldAction } from "../../actions/generateField.js";
|
|
||||||
|
|
||||||
export function AiFieldButton({ path, field }: { path: string; field: any }) {
|
export function AiFieldButton({ path, field }: { path: string; field: any }) {
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
const [instructions, setInstructions] = useState("");
|
const [instructions, setInstructions] = useState("");
|
||||||
@@ -44,19 +42,26 @@ export function AiFieldButton({ path, field }: { path: string; field: any }) {
|
|||||||
? field.admin.description
|
? field.admin.description
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const res = await generateSingleFieldAction(
|
const resData = await fetch("/api/api/mintel-ai/generate-single-field", {
|
||||||
(title as string) || "",
|
method: "POST",
|
||||||
draftContent,
|
headers: { "Content-Type": "application/json" },
|
||||||
fieldName,
|
body: JSON.stringify({
|
||||||
fieldDescription,
|
documentTitle: (title as string) || "",
|
||||||
instructions,
|
documentContent: draftContent,
|
||||||
);
|
fieldName,
|
||||||
|
fieldDescription,
|
||||||
|
instructions,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const res = await resData.json();
|
||||||
|
|
||||||
if (res.success && res.text) {
|
if (res.success && res.text) {
|
||||||
setValue(res.text);
|
setValue(res.text);
|
||||||
} else {
|
} else {
|
||||||
alert("Fehler: " + res.error);
|
alert("Fehler: " + res.error);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
alert("Fehler bei der Generierung.");
|
alert("Fehler bei der Generierung.");
|
||||||
} finally {
|
} finally {
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useForm, useField } from "@payloadcms/ui";
|
import { useForm, useField } from "@payloadcms/ui";
|
||||||
import { generateSlugAction } from "../../actions/generateField.js";
|
|
||||||
|
|
||||||
export function GenerateSlugButton({ path }: { path: string }) {
|
export function GenerateSlugButton({ path }: { path: string }) {
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
const [instructions, setInstructions] = useState("");
|
const [instructions, setInstructions] = useState("");
|
||||||
@@ -45,18 +43,24 @@ export function GenerateSlugButton({ path }: { path: string }) {
|
|||||||
|
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
try {
|
try {
|
||||||
const res = await generateSlugAction(
|
const resData = await fetch("/api/api/mintel-ai/generate-slug", {
|
||||||
title,
|
method: "POST",
|
||||||
draftContent,
|
headers: { "Content-Type": "application/json" },
|
||||||
initialValue as string,
|
body: JSON.stringify({
|
||||||
instructions,
|
title,
|
||||||
);
|
draftContent,
|
||||||
|
oldSlug: initialValue as string,
|
||||||
|
instructions,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const res = await resData.json();
|
||||||
|
|
||||||
if (res.success && res.slug) {
|
if (res.success && res.slug) {
|
||||||
setValue(res.slug);
|
setValue(res.slug);
|
||||||
} else {
|
} else {
|
||||||
alert("Fehler: " + res.error);
|
alert("Fehler: " + res.error);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Unerwarteter Fehler.");
|
alert("Unerwarteter Fehler.");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useForm, useField } from "@payloadcms/ui";
|
import { useForm, useField } from "@payloadcms/ui";
|
||||||
import { generateThumbnailAction } from "../../actions/generateField.js";
|
|
||||||
|
|
||||||
export function GenerateThumbnailButton({ path }: { path: string }) {
|
export function GenerateThumbnailButton({ path }: { path: string }) {
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
const [instructions, setInstructions] = useState("");
|
const [instructions, setInstructions] = useState("");
|
||||||
@@ -45,17 +43,23 @@ export function GenerateThumbnailButton({ path }: { path: string }) {
|
|||||||
|
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
try {
|
try {
|
||||||
const res = await generateThumbnailAction(
|
const resData = await fetch("/api/api/mintel-ai/generate-thumbnail", {
|
||||||
draftContent,
|
method: "POST",
|
||||||
title,
|
headers: { "Content-Type": "application/json" },
|
||||||
instructions,
|
body: JSON.stringify({
|
||||||
);
|
draftContent,
|
||||||
|
title,
|
||||||
|
instructions,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const res = await resData.json();
|
||||||
|
|
||||||
if (res.success && res.mediaId) {
|
if (res.success && res.mediaId) {
|
||||||
setValue(res.mediaId);
|
setValue(res.mediaId);
|
||||||
} else {
|
} else {
|
||||||
alert("Fehler: " + res.error);
|
alert("Fehler: " + res.error);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Unerwarteter Fehler.");
|
alert("Unerwarteter Fehler.");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useForm, useDocumentInfo } from "@payloadcms/ui";
|
import { useForm, useDocumentInfo } from "@payloadcms/ui";
|
||||||
import { optimizePostText } from "../actions/optimizePost.js";
|
|
||||||
import { Button } from "@payloadcms/ui";
|
import { Button } from "@payloadcms/ui";
|
||||||
|
|
||||||
export function OptimizeButton() {
|
export function OptimizeButton() {
|
||||||
@@ -57,7 +56,12 @@ export function OptimizeButton() {
|
|||||||
// 2. We inject the title so the AI knows what it's writing about
|
// 2. We inject the title so the AI knows what it's writing about
|
||||||
const payloadText = `---\ntitle: "${title}"\n---\n\n${draftContent}`;
|
const payloadText = `---\ntitle: "${title}"\n---\n\n${draftContent}`;
|
||||||
|
|
||||||
const response = await optimizePostText(payloadText, instructions);
|
const res = await fetch("/api/api/mintel-ai/optimize", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ draftContent: payloadText, instructions }),
|
||||||
|
});
|
||||||
|
const response = await res.json();
|
||||||
|
|
||||||
if (response.success && response.lexicalAST) {
|
if (response.success && response.lexicalAST) {
|
||||||
// 3. Inject the new Lexical AST directly into the field form state
|
// 3. Inject the new Lexical AST directly into the field form state
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const handleMcpChat = async (req: PayloadRequest) => {
|
|||||||
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 } = await req.json()
|
const { messages } = (await req.json?.() || { messages: [] }) as { messages: any[] }
|
||||||
|
|
||||||
// 1. Check AI Permissions for req.user
|
// 1. Check AI Permissions for req.user
|
||||||
// In a real implementation this looks up the global or collection for permissions
|
// In a real implementation this looks up the global or collection for permissions
|
||||||
@@ -67,7 +67,7 @@ export const handleMcpChat = async (req: PayloadRequest) => {
|
|||||||
${memorySystemPrompt}`
|
${memorySystemPrompt}`
|
||||||
})
|
})
|
||||||
|
|
||||||
return result.toDataStreamResponse()
|
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 })
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
"use server";
|
import { PayloadRequest } from "payload";
|
||||||
|
|
||||||
import { getPayloadHMR } from "@payloadcms/next/utilities";
|
|
||||||
import configPromise from "@payload-config";
|
|
||||||
import * as fs from "node:fs/promises";
|
import * as fs from "node:fs/promises";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
@@ -29,13 +26,9 @@ async function getOrchestrator() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateSlugAction(
|
export const generateSlugEndpoint = async (req: PayloadRequest) => {
|
||||||
title: string,
|
|
||||||
draftContent: string,
|
|
||||||
oldSlug?: string,
|
|
||||||
instructions?: string,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
|
const { title, draftContent, oldSlug, instructions } = (await req.json?.() || {}) as any;
|
||||||
const orchestrator = await getOrchestrator();
|
const orchestrator = await getOrchestrator();
|
||||||
const newSlug = await orchestrator.generateSlug(
|
const newSlug = await orchestrator.generateSlug(
|
||||||
draftContent,
|
draftContent,
|
||||||
@@ -44,9 +37,8 @@ export async function generateSlugAction(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (oldSlug && oldSlug !== newSlug) {
|
if (oldSlug && oldSlug !== newSlug) {
|
||||||
const payload = await getPayloadHMR({ config: configPromise as any });
|
await req.payload.create({
|
||||||
await payload.create({
|
collection: "redirects" as any,
|
||||||
collection: "redirects",
|
|
||||||
data: {
|
data: {
|
||||||
from: oldSlug,
|
from: oldSlug,
|
||||||
to: newSlug,
|
to: newSlug,
|
||||||
@@ -54,42 +46,25 @@ export async function generateSlugAction(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, slug: newSlug };
|
return Response.json({ success: true, slug: newSlug });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return { success: false, error: e.message };
|
return Response.json({ success: false, error: e.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateThumbnailAction(
|
export const generateThumbnailEndpoint = async (req: PayloadRequest) => {
|
||||||
draftContent: string,
|
|
||||||
title?: string,
|
|
||||||
instructions?: string,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const payload = await getPayloadHMR({ config: configPromise as any });
|
const { draftContent, title, instructions } = (await req.json?.() || {}) as any;
|
||||||
const OPENROUTER_KEY =
|
const OPENROUTER_KEY =
|
||||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||||
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||||
|
|
||||||
if (!OPENROUTER_KEY) {
|
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY in .env");
|
||||||
throw new Error("Missing OPENROUTER_API_KEY in .env");
|
if (!REPLICATE_KEY) throw new Error("Missing REPLICATE_API_KEY in .env");
|
||||||
}
|
|
||||||
if (!REPLICATE_KEY) {
|
|
||||||
throw new Error(
|
|
||||||
"Missing REPLICATE_API_KEY in .env (Required for Thumbnails)",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const importDynamic = new Function(
|
const importDynamic = new Function("modulePath", "return import(modulePath)");
|
||||||
"modulePath",
|
const { AiBlogPostOrchestrator } = await importDynamic("@mintel/content-engine");
|
||||||
"return import(modulePath)",
|
const { ThumbnailGenerator } = await importDynamic("@mintel/thumbnail-generator");
|
||||||
);
|
|
||||||
const { AiBlogPostOrchestrator } = await importDynamic(
|
|
||||||
"@mintel/content-engine",
|
|
||||||
);
|
|
||||||
const { ThumbnailGenerator } = await importDynamic(
|
|
||||||
"@mintel/thumbnail-generator",
|
|
||||||
);
|
|
||||||
|
|
||||||
const orchestrator = new AiBlogPostOrchestrator({
|
const orchestrator = new AiBlogPostOrchestrator({
|
||||||
apiKey: OPENROUTER_KEY,
|
apiKey: OPENROUTER_KEY,
|
||||||
@@ -111,8 +86,8 @@ export async function generateThumbnailAction(
|
|||||||
const stat = await fs.stat(tmpPath);
|
const stat = await fs.stat(tmpPath);
|
||||||
const fileName = path.basename(tmpPath);
|
const fileName = path.basename(tmpPath);
|
||||||
|
|
||||||
const newMedia = await payload.create({
|
const newMedia = await req.payload.create({
|
||||||
collection: "media",
|
collection: "media" as any,
|
||||||
data: {
|
data: {
|
||||||
alt: title ? `Thumbnail for ${title}` : "AI Generated Thumbnail",
|
alt: title ? `Thumbnail for ${title}` : "AI Generated Thumbnail",
|
||||||
},
|
},
|
||||||
@@ -124,31 +99,24 @@ export async function generateThumbnailAction(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup temp file
|
|
||||||
await fs.unlink(tmpPath).catch(() => { });
|
await fs.unlink(tmpPath).catch(() => { });
|
||||||
|
|
||||||
return { success: true, mediaId: newMedia.id };
|
return Response.json({ success: true, mediaId: newMedia.id });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return { success: false, error: e.message };
|
return Response.json({ success: false, error: e.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function generateSingleFieldAction(
|
|
||||||
documentTitle: string,
|
export const generateSingleFieldEndpoint = async (req: PayloadRequest) => {
|
||||||
documentContent: string,
|
|
||||||
fieldName: string,
|
|
||||||
fieldDescription: string,
|
|
||||||
instructions?: string,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
|
const { documentTitle, documentContent, fieldName, fieldDescription, instructions } = (await req.json?.() || {}) as any;
|
||||||
|
|
||||||
const OPENROUTER_KEY =
|
const OPENROUTER_KEY =
|
||||||
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY;
|
||||||
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY");
|
if (!OPENROUTER_KEY) throw new Error("Missing OPENROUTER_API_KEY");
|
||||||
|
|
||||||
const payload = await getPayloadHMR({ config: configPromise as any });
|
const contextDocsData = await req.payload.find({
|
||||||
|
collection: "context-files" as any,
|
||||||
// Fetch context documents from DB
|
|
||||||
const contextDocsData = await payload.find({
|
|
||||||
collection: "context-files",
|
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
const projectContext = contextDocsData.docs
|
const projectContext = contextDocsData.docs
|
||||||
@@ -183,8 +151,8 @@ CRITICAL RULES:
|
|||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const text = data.choices?.[0]?.message?.content?.trim() || "";
|
const text = data.choices?.[0]?.message?.content?.trim() || "";
|
||||||
return { success: true, text };
|
return Response.json({ success: true, text });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return { success: false, error: e.message };
|
return Response.json({ success: false, error: e.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
"use server";
|
import { PayloadRequest } from 'payload'
|
||||||
|
import { parseMarkdownToLexical } from "../utils/lexicalParser.js";
|
||||||
|
|
||||||
import { parseMarkdownToLexical } from "../utils/lexicalParser";
|
export const optimizePostEndpoint = async (req: PayloadRequest) => {
|
||||||
import { getPayloadHMR } from "@payloadcms/next/utilities";
|
|
||||||
import configPromise from "@payload-config";
|
|
||||||
|
|
||||||
export async function optimizePostText(
|
|
||||||
draftContent: string,
|
|
||||||
instructions?: string,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const payload = await getPayloadHMR({ config: configPromise as any });
|
const { draftContent, instructions } = (await req.json?.() || {}) as { draftContent: string; instructions?: string };
|
||||||
const globalAiSettings = (await payload.findGlobal({ slug: "ai-settings" })) as any;
|
|
||||||
|
if (!draftContent) {
|
||||||
|
return Response.json({ error: 'Missing draftContent' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalAiSettings = (await req.payload.findGlobal({ slug: "ai-settings" })) as any;
|
||||||
const customSources =
|
const customSources =
|
||||||
globalAiSettings?.customSources?.map((s: any) => s.sourceName) || [];
|
globalAiSettings?.customSources?.map((s: any) => s.sourceName) || [];
|
||||||
|
|
||||||
@@ -19,18 +18,12 @@ export async function optimizePostText(
|
|||||||
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
const REPLICATE_KEY = process.env.REPLICATE_API_KEY;
|
||||||
|
|
||||||
if (!OPENROUTER_KEY) {
|
if (!OPENROUTER_KEY) {
|
||||||
throw new Error(
|
return Response.json({ error: "OPENROUTER_KEY not found in environment." }, { status: 500 })
|
||||||
"OPENROUTER_KEY or OPENROUTER_API_KEY not found in environment.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const importDynamic = new Function(
|
// Dynamically import to avoid bundling it into client components that might accidentally import this file
|
||||||
"modulePath",
|
const importDynamic = new Function("modulePath", "return import(modulePath)");
|
||||||
"return import(modulePath)",
|
const { AiBlogPostOrchestrator } = await importDynamic("@mintel/content-engine");
|
||||||
);
|
|
||||||
const { AiBlogPostOrchestrator } = await importDynamic(
|
|
||||||
"@mintel/content-engine",
|
|
||||||
);
|
|
||||||
|
|
||||||
const orchestrator = new AiBlogPostOrchestrator({
|
const orchestrator = new AiBlogPostOrchestrator({
|
||||||
apiKey: OPENROUTER_KEY,
|
apiKey: OPENROUTER_KEY,
|
||||||
@@ -38,9 +31,8 @@ export async function optimizePostText(
|
|||||||
model: "google/gemini-3-flash-preview",
|
model: "google/gemini-3-flash-preview",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch context documents purely from DB
|
const contextDocsData = await req.payload.find({
|
||||||
const contextDocsData = await payload.find({
|
collection: "context-files" as any,
|
||||||
collection: "context-files",
|
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
const projectContext = contextDocsData.docs.map((doc: any) => doc.content);
|
const projectContext = contextDocsData.docs.map((doc: any) => doc.content);
|
||||||
@@ -48,19 +40,19 @@ export async function optimizePostText(
|
|||||||
const optimizedMarkdown = await orchestrator.optimizeDocument({
|
const optimizedMarkdown = await orchestrator.optimizeDocument({
|
||||||
content: draftContent,
|
content: draftContent,
|
||||||
projectContext,
|
projectContext,
|
||||||
availableComponents: [], // Removed hardcoded config.components dependency
|
availableComponents: [],
|
||||||
instructions,
|
instructions,
|
||||||
internalLinks: [],
|
internalLinks: [],
|
||||||
customSources,
|
customSources,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!optimizedMarkdown || typeof optimizedMarkdown !== "string") {
|
if (!optimizedMarkdown || typeof optimizedMarkdown !== "string") {
|
||||||
throw new Error("AI returned invalid markup.");
|
return Response.json({ error: "AI returned invalid markup." }, { status: 500 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const blocks = parseMarkdownToLexical(optimizedMarkdown);
|
const blocks = parseMarkdownToLexical(optimizedMarkdown);
|
||||||
|
|
||||||
return {
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
lexicalAST: {
|
lexicalAST: {
|
||||||
root: {
|
root: {
|
||||||
@@ -72,12 +64,12 @@ export async function optimizePostText(
|
|||||||
direction: "ltr",
|
direction: "ltr",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Failed to optimize post:", error);
|
console.error("Failed to optimize post in endpoint:", error);
|
||||||
return {
|
return Response.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message || "An unknown error occurred during optimization.",
|
error: error.message || "An unknown error occurred during optimization.",
|
||||||
};
|
}, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,13 +3,17 @@
|
|||||||
* Primary entry point for reusing Mintel AI extensions in Payload CMS.
|
* Primary entry point for reusing Mintel AI extensions in Payload CMS.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './globals/AiSettings';
|
export * from './globals/AiSettings.js';
|
||||||
export * from './actions/generateField';
|
export * from './components/FieldGenerators/AiFieldButton.js';
|
||||||
export * from './actions/optimizePost';
|
export * from './components/AiMediaButtons.js';
|
||||||
export * from './components/FieldGenerators/AiFieldButton';
|
export * from './components/OptimizeButton.js';
|
||||||
export * from './components/AiMediaButtons';
|
export * from './components/FieldGenerators/GenerateThumbnailButton.js';
|
||||||
export * from './components/OptimizeButton';
|
export * from './components/FieldGenerators/GenerateSlugButton.js';
|
||||||
export * from './components/FieldGenerators/GenerateThumbnailButton';
|
export * from './utils/lexicalParser.js';
|
||||||
export * from './components/FieldGenerators/GenerateSlugButton';
|
export * from './endpoints/replicateMediaEndpoint.js';
|
||||||
export * from './utils/lexicalParser';
|
export * from './chatPlugin.js';
|
||||||
export * from './endpoints/replicateMediaEndpoint';
|
export * from './types.js';
|
||||||
|
export * from './endpoints/chatEndpoint.js';
|
||||||
|
export * from './tools/mcpAdapter.js';
|
||||||
|
export * from './tools/memoryDb.js';
|
||||||
|
export * from './tools/payloadLocal.js';
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ 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.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) => {
|
execute: async (args: any) => {
|
||||||
const result = await client.callTool({
|
const result = await client.callTool({
|
||||||
name: extTool.name,
|
name: extTool.name,
|
||||||
@@ -15,7 +15,7 @@ const MEMORY_COLLECTION = 'mintel_ai_memory'
|
|||||||
async function initQdrant() {
|
async function initQdrant() {
|
||||||
try {
|
try {
|
||||||
const res = await qdrantClient.getCollections()
|
const res = await qdrantClient.getCollections()
|
||||||
const exists = res.collections.find((c) => 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: {
|
||||||
@@ -47,7 +47,8 @@ export const generateMemoryTools = (userId: string | number) => {
|
|||||||
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".'),
|
||||||
}),
|
}),
|
||||||
execute: async ({ fact, category }) => {
|
// @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
|
// 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)
|
||||||
@@ -84,7 +85,8 @@ export const generateMemoryTools = (userId: string | number) => {
|
|||||||
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.'),
|
||||||
}),
|
}),
|
||||||
execute: async ({ query }) => {
|
// @ts-ignore - AI SDK strict mode bug
|
||||||
|
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())
|
||||||
|
|
||||||
@@ -102,7 +104,7 @@ export const generateMemoryTools = (userId: string | number) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return results.map(r => 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 []
|
||||||
@@ -22,7 +22,8 @@ export const generatePayloadLocalTools = (
|
|||||||
// 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.'),
|
||||||
}),
|
}),
|
||||||
execute: async ({ limit = 10, page = 1, query }) => {
|
// @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
|
const where = query ? { id: { equals: query } } : undefined // Placeholder logic
|
||||||
|
|
||||||
return await payload.find({
|
return await payload.find({
|
||||||
@@ -41,7 +42,8 @@ export const generatePayloadLocalTools = (
|
|||||||
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.'),
|
||||||
}),
|
}),
|
||||||
execute: async ({ id }) => {
|
// @ts-ignore - AI SDK strict mode type inference bug
|
||||||
|
execute: async ({ id }: { id: string | number }) => {
|
||||||
return await payload.findByID({
|
return await payload.findByID({
|
||||||
collection: collectionSlug as any,
|
collection: collectionSlug as any,
|
||||||
id,
|
id,
|
||||||
@@ -56,7 +58,8 @@ export const generatePayloadLocalTools = (
|
|||||||
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.'),
|
||||||
}),
|
}),
|
||||||
execute: async ({ data }) => {
|
// @ts-ignore - AI SDK strict mode type inference bug
|
||||||
|
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,
|
||||||
@@ -72,7 +75,8 @@ export const generatePayloadLocalTools = (
|
|||||||
id: z.union([z.string(), z.number()]).describe('The ID of the document to update.'),
|
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.'),
|
data: z.record(z.any()).describe('A JSON object containing the fields to update.'),
|
||||||
}),
|
}),
|
||||||
execute: async ({ id, data }) => {
|
// @ts-ignore - AI SDK strict mode type inference bug
|
||||||
|
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,
|
||||||
@@ -88,7 +92,8 @@ export const generatePayloadLocalTools = (
|
|||||||
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.'),
|
||||||
}),
|
}),
|
||||||
execute: async ({ id }) => {
|
// @ts-ignore - AI SDK strict mode type inference bug
|
||||||
|
execute: async ({ id }: { id: string | number }) => {
|
||||||
return await payload.delete({
|
return await payload.delete({
|
||||||
collection: collectionSlug as any,
|
collection: collectionSlug as any,
|
||||||
id,
|
id,
|
||||||
11
packages/payload-ai/src/types.d.ts
vendored
11
packages/payload-ai/src/types.d.ts
vendored
@@ -1,5 +1,8 @@
|
|||||||
declare module "@payload-config" {
|
export type PayloadChatPluginConfig = {
|
||||||
import { Config } from "payload";
|
enabled?: boolean
|
||||||
const configPromise: Promise<Config>;
|
/** Render the chat bubble on the bottom right? Defaults to true */
|
||||||
export default configPromise;
|
renderChatBubble?: boolean
|
||||||
|
allowedCollections?: string[]
|
||||||
|
mcpServers?: any[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Plugin } from 'payload'
|
import type { Plugin } from 'payload'
|
||||||
|
|
||||||
export interface PayloadMcpChatPluginConfig {
|
export interface PayloadChatPluginConfig {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
/**
|
/**
|
||||||
* Defines whether to render the floating chat bubble in the admin panel automatically.
|
* Defines whether to render the floating chat bubble in the admin panel automatically.
|
||||||
@@ -12,15 +12,24 @@
|
|||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
|
"baseUrl": ".",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"sourceMap": true
|
"sourceMap": true,
|
||||||
|
"paths": {
|
||||||
|
"@payload-config": [
|
||||||
|
"../../apps/mintel.me/payload.config.ts",
|
||||||
|
"../../apps/web/payload.config.ts",
|
||||||
|
"./node_modules/@payloadcms/next/dist/index.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*"
|
"src/**/*",
|
||||||
|
"src/types.d.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@mintel/payload-chat",
|
|
||||||
"version": "1.9.9",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# @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.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { payloadMcpChatPlugin } from './plugin.js'
|
|
||||||
export type { PayloadMcpChatPluginConfig } from './types.js'
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/pdf",
|
"name": "@mintel/pdf",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/seo-engine",
|
"name": "@mintel/seo-engine",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"private": true,
|
|
||||||
"description": "AI-powered SEO keyword and topic cluster evaluation engine",
|
"description": "AI-powered SEO keyword and topic cluster evaluation engine",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/thumbnail-generator",
|
"name": "@mintel/thumbnail-generator",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"private": false,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mintel/tsconfig",
|
"name": "@mintel/tsconfig",
|
||||||
"version": "1.9.9",
|
"version": "1.9.10",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
"registry": "https://git.infra.mintel.me/api/packages/mmintel/npm"
|
||||||
|
|||||||
675
pnpm-lock.yaml
generated
675
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user