import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import express, { Request, Response } from 'express'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; import https from "https"; const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://klz-cables.com"; const PAYLOAD_API_KEY = process.env.PAYLOAD_API_KEY; const PAYLOAD_EMAIL = process.env.PAYLOAD_EMAIL || "agent@mintel.me"; const PAYLOAD_PASSWORD = process.env.PAYLOAD_PASSWORD || "agentpassword123"; const httpsAgent = new https.Agent({ rejectUnauthorized: false, // For internal infra }); let jwtToken: string | null = null; const payloadClient = axios.create({ baseURL: `${PAYLOAD_URL}/api`, headers: PAYLOAD_API_KEY ? { Authorization: `users API-Key ${PAYLOAD_API_KEY}` } : {}, httpsAgent }); payloadClient.interceptors.request.use(async (config) => { if (!PAYLOAD_API_KEY && !jwtToken && PAYLOAD_EMAIL && PAYLOAD_PASSWORD) { try { const loginRes = await axios.post(`${PAYLOAD_URL}/api/users/login`, { email: PAYLOAD_EMAIL, password: PAYLOAD_PASSWORD }, { httpsAgent }); if (loginRes.data && loginRes.data.token) { jwtToken = loginRes.data.token; } } catch (e) { console.error("Failed to authenticate with Payload CMS using email/password."); } } if (jwtToken && !PAYLOAD_API_KEY) { config.headers.Authorization = `JWT ${jwtToken}`; } return config; }); payloadClient.interceptors.response.use(res => res, async (error) => { const originalRequest = error.config; // If token expired, clear it and retry if (error.response?.status === 401 && !originalRequest._retry && !PAYLOAD_API_KEY) { originalRequest._retry = true; jwtToken = null; // Forces re-authentication on next interceptor run return payloadClient(originalRequest); } return Promise.reject(error); }); const SEARCH_PRODUCTS_TOOL: Tool = { name: "payload_search_products", description: "Search for technical product specifications (cables, cross-sections) in KLZ Payload CMS", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query or part number" }, limit: { type: "number", description: "Maximum number of results" }, }, }, }; const GET_PRODUCT_TOOL: Tool = { name: "payload_get_product", description: "Get a specific product by its slug or ID", inputSchema: { type: "object", properties: { slug: { type: "string", description: "Product slug" }, id: { type: "string", description: "Product ID (if slug is not used)" } }, }, }; const CREATE_PRODUCT_TOOL: Tool = { name: "payload_create_product", description: "Create a new product in KLZ Payload CMS", inputSchema: { type: "object", properties: { title: { type: "string", description: "Product title" }, slug: { type: "string", description: "Product slug" }, data: { type: "object", description: "Additional product data (JSON)", additionalProperties: true } }, required: ["title"] }, }; const UPDATE_PRODUCT_TOOL: Tool = { name: "payload_update_product", description: "Update an existing product in KLZ Payload CMS", inputSchema: { type: "object", properties: { id: { type: "string", description: "Product ID to update" }, data: { type: "object", description: "Product data to update (JSON)", additionalProperties: true } }, required: ["id", "data"] }, }; const DELETE_PRODUCT_TOOL: Tool = { name: "payload_delete_product", description: "Delete a product from KLZ Payload CMS", inputSchema: { type: "object", properties: { id: { type: "string", description: "Product ID to delete" } }, required: ["id"] }, }; const LIST_LEADS_TOOL: Tool = { name: "payload_list_leads", description: "List recent lead inquiries and contact requests", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Maximum number of leads" }, }, }, }; const GET_LEAD_TOOL: Tool = { name: "payload_get_lead", description: "Get a specific lead by ID", inputSchema: { type: "object", properties: { id: { type: "string", description: "Lead ID" } }, required: ["id"] }, }; const CREATE_LEAD_TOOL: Tool = { name: "payload_create_lead", description: "Create a new lead in KLZ Payload CMS", inputSchema: { type: "object", properties: { email: { type: "string", description: "Lead email address" }, data: { type: "object", description: "Additional lead data (JSON)", additionalProperties: true } }, required: ["email"] }, }; const UPDATE_LEAD_TOOL: Tool = { name: "payload_update_lead", description: "Update an existing lead in KLZ Payload CMS", inputSchema: { type: "object", properties: { id: { type: "string", description: "Lead ID to update" }, data: { type: "object", description: "Lead data to update (JSON)", additionalProperties: true } }, required: ["id", "data"] }, }; const DELETE_LEAD_TOOL: Tool = { name: "payload_delete_lead", description: "Delete a lead from KLZ Payload CMS", inputSchema: { type: "object", properties: { id: { type: "string", description: "Lead ID to delete" } }, required: ["id"] }, }; const LIST_PAGES_TOOL: Tool = { name: "payload_list_pages", description: "List pages from KLZ Payload CMS", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Maximum number of pages" }, }, }, }; const GET_PAGE_TOOL: Tool = { name: "payload_get_page", description: "Get a specific page by its slug or ID", inputSchema: { type: "object", properties: { slug: { type: "string", description: "Page slug" }, id: { type: "string", description: "Page ID (if slug is not used)" } }, }, }; const LIST_POSTS_TOOL: Tool = { name: "payload_list_posts", description: "List posts/articles from KLZ Payload CMS", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Maximum number of posts" }, }, }, }; const GET_POST_TOOL: Tool = { name: "payload_get_post", description: "Get a specific post by its slug or ID", inputSchema: { type: "object", properties: { slug: { type: "string", description: "Post slug" }, id: { type: "string", description: "Post ID (if slug is not used)" } }, }, }; const CREATE_PAGE_TOOL: Tool = { name: "payload_create_page", description: "Create a new page in KLZ Payload CMS", inputSchema: { type: "object", properties: { title: { type: "string", description: "Page title" }, slug: { type: "string", description: "Page slug" }, data: { type: "object", description: "Additional page data (JSON)", additionalProperties: true } }, required: ["title"] }, }; const UPDATE_PAGE_TOOL: Tool = { name: "payload_update_page", description: "Update an existing page in KLZ Payload CMS", inputSchema: { type: "object", properties: { id: { type: "string", description: "Page ID to update" }, data: { type: "object", description: "Page data to update (JSON)", additionalProperties: true } }, required: ["id", "data"] }, }; const DELETE_PAGE_TOOL: Tool = { name: "payload_delete_page", description: "Delete a page from KLZ Payload CMS", inputSchema: { type: "object", properties: { id: { type: "string", description: "Page ID to delete" } }, required: ["id"] }, }; const CREATE_POST_TOOL: Tool = { name: "payload_create_post", description: "Create a new post in KLZ Payload CMS", inputSchema: { type: "object", properties: { title: { type: "string", description: "Post title" }, slug: { type: "string", description: "Post slug" }, data: { type: "object", description: "Additional post data (JSON)", additionalProperties: true } }, required: ["title"] }, }; const UPDATE_POST_TOOL: Tool = { name: "payload_update_post", description: "Update an existing post in KLZ Payload CMS", inputSchema: { type: "object", properties: { id: { type: "string", description: "Post ID to update" }, data: { type: "object", description: "Post data to update (JSON)", additionalProperties: true } }, required: ["id", "data"] }, }; const DELETE_POST_TOOL: Tool = { name: "payload_delete_post", description: "Delete a post from KLZ Payload CMS", inputSchema: { type: "object", properties: { id: { type: "string", description: "Post ID to delete" } }, required: ["id"] }, }; const server = new Server( { name: "klz-payload-mcp", version: "1.0.0" }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ SEARCH_PRODUCTS_TOOL, GET_PRODUCT_TOOL, CREATE_PRODUCT_TOOL, UPDATE_PRODUCT_TOOL, DELETE_PRODUCT_TOOL, LIST_LEADS_TOOL, GET_LEAD_TOOL, CREATE_LEAD_TOOL, UPDATE_LEAD_TOOL, DELETE_LEAD_TOOL, LIST_PAGES_TOOL, GET_PAGE_TOOL, CREATE_PAGE_TOOL, UPDATE_PAGE_TOOL, DELETE_PAGE_TOOL, LIST_POSTS_TOOL, GET_POST_TOOL, CREATE_POST_TOOL, UPDATE_POST_TOOL, DELETE_POST_TOOL ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "payload_search_products") { const { query, limit = 10 } = request.params.arguments as any; try { const res = await payloadClient.get('/products', { params: { where: query ? { or: [ { title: { contains: query } }, { slug: { contains: query } }, { description: { contains: query } } ] } : {}, limit } }); return { content: [{ type: "text", text: JSON.stringify(res.data.docs, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] }; } } if (request.params.name === "payload_get_product") { const { slug, id } = request.params.arguments as any; try { if (id) { const res = await payloadClient.get(`/products/${id}`); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } else if (slug) { const res = await payloadClient.get('/products', { params: { where: { slug: { equals: slug } }, limit: 1 } }); return { content: [{ type: "text", text: JSON.stringify(res.data.docs[0] || {}, null, 2) }] }; } return { isError: true, content: [{ type: "text", text: "Error: must provide slug or id" }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] }; } } if (request.params.name === "payload_create_product") { const { title, slug, data = {} } = request.params.arguments as any; try { const payload = { title, slug, _status: 'draft', ...data }; const res = await payloadClient.post('/products', payload); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] }; } } if (request.params.name === "payload_update_product") { const { id, data } = request.params.arguments as any; try { const res = await payloadClient.patch(`/products/${id}`, data); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] }; } } if (request.params.name === "payload_delete_product") { const { id } = request.params.arguments as any; try { const res = await payloadClient.delete(`/products/${id}`); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] }; } } if (request.params.name === "payload_list_leads") { const { limit = 10 } = request.params.arguments as any; try { const res = await payloadClient.get('/leads', { params: { limit, sort: '-createdAt' } }); return { content: [{ type: "text", text: JSON.stringify(res.data.docs, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] }; } } if (request.params.name === "payload_get_lead") { const { id } = request.params.arguments as any; try { const res = await payloadClient.get(`/leads/${id}`); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] }; } } if (request.params.name === "payload_create_lead") { const { email, data = {} } = request.params.arguments as any; try { const payload = { email, ...data }; const res = await payloadClient.post('/leads', payload); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] }; } } if (request.params.name === "payload_update_lead") { const { id, data } = request.params.arguments as any; try { const res = await payloadClient.patch(`/leads/${id}`, data); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] }; } } if (request.params.name === "payload_delete_lead") { const { id } = request.params.arguments as any; try { const res = await payloadClient.delete(`/leads/${id}`); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] }; } } if (request.params.name === "payload_list_pages") { const { limit = 10 } = request.params.arguments as any; try { const res = await payloadClient.get('/pages', { params: { limit } }); return { content: [{ type: "text", text: JSON.stringify(res.data.docs, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] }; } } if (request.params.name === "payload_get_page") { const { slug, id } = request.params.arguments as any; try { if (id) { const res = await payloadClient.get(`/pages/${id}`); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } else if (slug) { const res = await payloadClient.get('/pages', { params: { where: { slug: { equals: slug } }, limit: 1 } }); return { content: [{ type: "text", text: JSON.stringify(res.data.docs[0] || {}, null, 2) }] }; } return { isError: true, content: [{ type: "text", text: "Error: must provide slug or id" }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] }; } } if (request.params.name === "payload_create_page") { const { title, slug, data = {} } = request.params.arguments as any; try { const payload = { title, slug, _status: 'draft', ...data }; const res = await payloadClient.post('/pages', payload); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] }; } } if (request.params.name === "payload_update_page") { const { id, data } = request.params.arguments as any; try { const res = await payloadClient.patch(`/pages/${id}`, data); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] }; } } if (request.params.name === "payload_delete_page") { const { id } = request.params.arguments as any; try { const res = await payloadClient.delete(`/pages/${id}`); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] }; } } if (request.params.name === "payload_list_posts") { const { limit = 10 } = request.params.arguments as any; try { const res = await payloadClient.get('/posts', { params: { limit, sort: '-createdAt' } }); return { content: [{ type: "text", text: JSON.stringify(res.data.docs, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] }; } } if (request.params.name === "payload_get_post") { const { slug, id } = request.params.arguments as any; try { if (id) { const res = await payloadClient.get(`/posts/${id}`); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } else if (slug) { const res = await payloadClient.get('/posts', { params: { where: { slug: { equals: slug } }, limit: 1 } }); return { content: [{ type: "text", text: JSON.stringify(res.data.docs[0] || {}, null, 2) }] }; } return { isError: true, content: [{ type: "text", text: "Error: must provide slug or id" }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${e.response?.data?.errors?.[0]?.message || e.message}` }] }; } } if (request.params.name === "payload_create_post") { const { title, slug, data = {} } = request.params.arguments as any; try { const payload = { title, slug, _status: 'draft', ...data }; const res = await payloadClient.post('/posts', payload); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] }; } } if (request.params.name === "payload_update_post") { const { id, data } = request.params.arguments as any; try { const res = await payloadClient.patch(`/posts/${id}`, data); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] }; } } if (request.params.name === "payload_delete_post") { const { id } = request.params.arguments as any; try { const res = await payloadClient.delete(`/posts/${id}`); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: `Error: ${JSON.stringify(e.response?.data) || e.message}` }] }; } } throw new Error(`Unknown tool: ${request.params.name}`); }); async function run() { const isStdio = process.argv.includes('--stdio'); if (isStdio) { const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js'); const transport = new StdioServerTransport(); await server.connect(transport); console.error('KLZ Payload MCP server is running on stdio'); } else { const app = express(); let transport: SSEServerTransport | null = null; app.get('/sse', async (req: Request, res: Response) => { console.error('New SSE connection established'); transport = new SSEServerTransport('/message', res); await server.connect(transport); }); app.post('/message', async (req: Request, res: Response) => { if (!transport) { res.status(400).send('No active SSE connection'); return; } await transport.handlePostMessage(req, res); }); const PORT = process.env.KLZ_PAYLOAD_MCP_PORT || 3006; app.listen(PORT, () => { console.error(`KLZ Payload MCP server running on http://localhost:${PORT}/sse`); }); } } run().catch((err) => { console.error("Fatal error:", err); process.exit(1); });