Spaces:
Paused
Paused
| import express from 'express'; | |
| import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; | |
| import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; | |
| import { z } from 'zod'; | |
| import path from 'path'; | |
| import fs from 'fs'; | |
| import { fileURLToPath } from 'url'; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const PORT = parseInt(process.env.PORT || '7860'); | |
| const BASE_URL = (process.env.SPACE_URL || `http://localhost:${PORT}`).replace(/\/$/, ''); | |
| const DOWNLOADS_DIR = path.join(__dirname, 'downloads'); | |
| if (!fs.existsSync(DOWNLOADS_DIR)) fs.mkdirSync(DOWNLOADS_DIR, { recursive: true }); | |
| // ── Lazy imports ────────────────────────────────────────────── | |
| async function getPdfLib() { | |
| const pdfLib = await import('pdf-lib'); | |
| const fontkit = await import('@pdf-lib/fontkit'); | |
| return { pdfLib, fontkit: fontkit.default }; | |
| } | |
| async function getPdfMake() { return (await import('pdfmake')).default; } | |
| async function getPuppeteer() { return (await import('puppeteer')).default; } | |
| async function getPdfParse() { return (await import('pdf-parse')).default; } | |
| function savePath(filename) { | |
| const safe = filename.endsWith('.pdf') ? filename : `${filename}.pdf`; | |
| return path.join(DOWNLOADS_DIR, safe); | |
| } | |
| function dlUrl(filename) { | |
| const safe = filename.endsWith('.pdf') ? filename : `${filename}.pdf`; | |
| return `${BASE_URL}/download/${safe}`; | |
| } | |
| async function fetchBytes(urlOrPath) { | |
| if (urlOrPath.startsWith('http')) { | |
| const fetch = (await import('node-fetch')).default; | |
| const r = await fetch(urlOrPath); | |
| return Buffer.from(await r.arrayBuffer()); | |
| } | |
| return fs.readFileSync(urlOrPath); | |
| } | |
| function hexToRgb(hex) { | |
| return { | |
| r: parseInt(hex.slice(1,3),16)/255, | |
| g: parseInt(hex.slice(3,5),16)/255, | |
| b: parseInt(hex.slice(5,7),16)/255 | |
| }; | |
| } | |
| function getPdfMakeFonts() { | |
| const fontsDir = path.join(__dirname, 'fonts'); | |
| if (!fs.existsSync(fontsDir)) return {}; | |
| const n = path.join(fontsDir, 'Roboto-Regular.ttf'); | |
| if (!fs.existsSync(n)) return {}; | |
| return { Roboto: { | |
| normal: n, | |
| bold: path.join(fontsDir, 'Roboto-Bold.ttf'), | |
| italics: path.join(fontsDir, 'Roboto-Italic.ttf'), | |
| bolditalics: path.join(fontsDir, 'Roboto-BoldItalic.ttf'), | |
| }}; | |
| } | |
| // ── Express ─────────────────────────────────────────────────── | |
| const app = express(); | |
| app.use(express.json({ limit: '50mb' })); | |
| app.use('/download', express.static(DOWNLOADS_DIR)); | |
| app.use((req, res, next) => { | |
| res.setHeader('Access-Control-Allow-Origin', '*'); | |
| res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE'); | |
| res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, mcp-session-id'); | |
| if (req.method === 'OPTIONS') { res.sendStatus(200); return; } | |
| next(); | |
| }); | |
| app.get('/', (req, res) => res.json({ name: 'PDF MCP Server', tools: 40, mcp: `${BASE_URL}/mcp` })); | |
| app.get('/files', (req, res) => { | |
| const files = fs.readdirSync(DOWNLOADS_DIR).map(f => ({ | |
| name: f, url: `${BASE_URL}/download/${f}`, | |
| size: `${(fs.statSync(path.join(DOWNLOADS_DIR,f)).size/1024).toFixed(1)}KB` | |
| })); | |
| res.json({ count: files.length, files }); | |
| }); | |
| // ── MCP ─────────────────────────────────────────────────────── | |
| app.all('/mcp', async (req, res) => { | |
| console.log(`[MCP] ${req.method}`); | |
| const server = new McpServer({ name: 'pdf-mcp-server', version: '1.0.0' }); | |
| // ═══════════════════════════════════════════════ | |
| // GROUP 1: pdfmake — Creation + Tables (10 tools) | |
| // ═══════════════════════════════════════════════ | |
| server.tool('create_pdf', | |
| 'নতুন PDF তৈরি করে। title, content array (text/heading/list blocks), pageSize, pageOrientation দিয়ে সম্পূর্ণ PDF বানায়। Download link return করে।', | |
| { filename: z.string(), title: z.string().optional(), content: z.array(z.object({ type: z.string(), value: z.any(), style: z.any().optional() })), pageSize: z.string().optional(), pageOrientation: z.string().optional() }, | |
| async ({ filename, title, content, pageSize, pageOrientation }) => { | |
| const PdfPrinter = await getPdfMake(); | |
| const fonts = getPdfMakeFonts(); | |
| const printer = new PdfPrinter(fonts); | |
| const blocks = (content||[]).map(b => { | |
| if (b.type==='heading') return { text: b.value, fontSize:16, bold:true, margin:[0,10,0,5] }; | |
| if (b.type==='list') return { ul: Array.isArray(b.value)?b.value:[b.value], margin:[0,4,0,4] }; | |
| return { text: b.value||b, margin:[0,2,0,2] }; | |
| }); | |
| const docDef = { | |
| pageSize: pageSize||'A4', pageOrientation: pageOrientation||'portrait', | |
| content: [ | |
| title ? { text:title, fontSize:20, bold:true, alignment:'center', margin:[0,0,0,20] } : null, | |
| ...blocks | |
| ].filter(Boolean) | |
| }; | |
| const out = savePath(filename); | |
| await new Promise((res2,rej) => { | |
| const doc = printer.createPdfKitDocument(docDef); | |
| const st = fs.createWriteStream(out); | |
| doc.pipe(st); doc.end(); | |
| st.on('finish', res2); st.on('error', rej); | |
| }); | |
| return { content: [{ type:'text', text:`✅ PDF তৈরি!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('create_pdf_with_table', | |
| 'Complex table সহ PDF তৈরি করে। headers, rows, colspan, colors সহ পূর্ণ table support। Invoice, report, schedule সব কাজে ব্যবহারযোগ্য।', | |
| { filename: z.string(), title: z.string().optional(), tables: z.array(z.object({ headers: z.array(z.string()), rows: z.array(z.array(z.any())), widths: z.array(z.string()).optional(), headerColor: z.string().optional() })), headerText: z.string().optional(), footerText: z.string().optional() }, | |
| async ({ filename, title, tables, headerText, footerText }) => { | |
| const PdfPrinter = await getPdfMake(); | |
| const printer = new PdfPrinter(getPdfMakeFonts()); | |
| const tblBlocks = (tables||[]).map(t => ({ | |
| table: { | |
| headerRows:1, | |
| widths: t.widths||t.headers.map(()=>'*'), | |
| body: [ | |
| t.headers.map(h=>({ text:h, bold:true, fillColor:t.headerColor||'#4472C4', color:'#FFF', alignment:'center', margin:[4,6,4,6] })), | |
| ...t.rows.map((row,ri)=>row.map(cell=>({ text:String(cell), fillColor:ri%2===0?'#F2F2F2':'#FFF', margin:[4,4,4,4] }))) | |
| ] | |
| }, layout:'lightHorizontalLines', margin:[0,10,0,10] | |
| })); | |
| const docDef = { | |
| pageSize:'A4', defaultStyle:{ fontSize:10 }, | |
| content: [ | |
| title ? { text:title, fontSize:18, bold:true, alignment:'center', margin:[0,0,0,10] } : null, | |
| headerText ? { text:headerText, margin:[0,0,0,10] } : null, | |
| ...tblBlocks, | |
| footerText ? { text:footerText, margin:[0,10,0,0] } : null, | |
| ].filter(Boolean) | |
| }; | |
| const out = savePath(filename); | |
| await new Promise((res2,rej) => { | |
| const doc = printer.createPdfKitDocument(docDef); | |
| const st = fs.createWriteStream(out); | |
| doc.pipe(st); doc.end(); | |
| st.on('finish',res2); st.on('error',rej); | |
| }); | |
| return { content: [{ type:'text', text:`✅ Table PDF তৈরি!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('create_invoice', | |
| 'Professional invoice/bill PDF তৈরি করে। company, client info, items (quantity×price), tax, total সহ। BDT currency support।', | |
| { filename:z.string(), invoiceNumber:z.string().optional(), date:z.string().optional(), company:z.object({ name:z.string(), address:z.string().optional(), phone:z.string().optional(), email:z.string().optional() }), client:z.object({ name:z.string(), address:z.string().optional(), phone:z.string().optional() }), items:z.array(z.object({ description:z.string(), quantity:z.number(), unitPrice:z.number() })), taxRate:z.number().optional(), currency:z.string().optional(), notes:z.string().optional() }, | |
| async ({ filename, invoiceNumber, date, company, client, items, taxRate=0, currency='BDT ৳', notes }) => { | |
| const PdfPrinter = await getPdfMake(); | |
| const printer = new PdfPrinter(getPdfMakeFonts()); | |
| const subtotal = items.reduce((s,i)=>s+i.quantity*i.unitPrice,0); | |
| const tax = subtotal*taxRate/100; | |
| const total = subtotal+tax; | |
| const docDef = { | |
| pageSize:'A4', defaultStyle:{fontSize:10}, | |
| content:[ | |
| { columns:[ { text:company.name, fontSize:22, bold:true, color:'#2C3E50' }, { text:`INVOICE\n#${invoiceNumber||'INV-001'}`, alignment:'right', fontSize:16, bold:true, color:'#E74C3C' } ] }, | |
| { text:company.address||'', color:'#666', margin:[0,4,0,0] }, | |
| { text:`${company.phone||''} | ${company.email||''}`, color:'#666', margin:[0,2,0,20] }, | |
| { columns:[ [{ text:'BILL TO:', bold:true, margin:[0,10,0,5] }, { text:client.name, bold:true }, { text:client.address||'' }, { text:client.phone||'' }], [{ text:`Date: ${date||new Date().toLocaleDateString()}`, alignment:'right', margin:[0,10,0,5] }] ] }, | |
| { margin:[0,20,0,0], table:{ headerRows:1, widths:['*','auto','auto','auto'], body:[ | |
| [{ text:'Description', bold:true, fillColor:'#2C3E50', color:'#FFF', margin:[4,6,4,6] },{ text:'Qty', bold:true, fillColor:'#2C3E50', color:'#FFF', margin:[4,6,4,6] },{ text:'Unit Price', bold:true, fillColor:'#2C3E50', color:'#FFF', margin:[4,6,4,6] },{ text:'Amount', bold:true, fillColor:'#2C3E50', color:'#FFF', margin:[4,6,4,6] }], | |
| ...items.map((item,i)=>[ { text:item.description, fillColor:i%2===0?'#F8F9FA':'white', margin:[4,4,4,4] }, { text:String(item.quantity), alignment:'center', fillColor:i%2===0?'#F8F9FA':'white', margin:[4,4,4,4] }, { text:`${currency} ${item.unitPrice.toFixed(2)}`, alignment:'right', fillColor:i%2===0?'#F8F9FA':'white', margin:[4,4,4,4] }, { text:`${currency} ${(item.quantity*item.unitPrice).toFixed(2)}`, alignment:'right', fillColor:i%2===0?'#F8F9FA':'white', margin:[4,4,4,4] } ]) | |
| ] }, layout:'lightHorizontalLines' }, | |
| { columns:[ {text:''}, { width:200, table:{ widths:['*','auto'], body:[ [{ text:'Subtotal:', bold:true },{ text:`${currency} ${subtotal.toFixed(2)}`, alignment:'right' }], taxRate>0?[{ text:`Tax (${taxRate}%):`, bold:true },{ text:`${currency} ${tax.toFixed(2)}`, alignment:'right' }]:null, [{ text:'TOTAL:', bold:true, fontSize:14, color:'#E74C3C' },{ text:`${currency} ${total.toFixed(2)}`, bold:true, fontSize:14, color:'#E74C3C', alignment:'right' }] ].filter(Boolean) }, layout:'noBorders', margin:[0,20,0,0] } ] }, | |
| notes ? { text:`Notes: ${notes}`, color:'#666', margin:[0,20,0,0], italics:true } : null, | |
| ].filter(Boolean) | |
| }; | |
| const out = savePath(filename); | |
| await new Promise((res2,rej) => { const doc = printer.createPdfKitDocument(docDef); const st=fs.createWriteStream(out); doc.pipe(st); doc.end(); st.on('finish',res2); st.on('error',rej); }); | |
| return { content:[{ type:'text', text:`✅ Invoice তৈরি!\n💰 Total: ${currency} ${total.toFixed(2)}\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('create_report', | |
| 'বিস্তারিত report PDF তৈরি করে। Cover page, sections with headings, data tables, page numbers সহ professional format।', | |
| { filename:z.string(), title:z.string(), subtitle:z.string().optional(), author:z.string().optional(), date:z.string().optional(), sections:z.array(z.object({ heading:z.string(), body:z.string() })), includePageNumbers:z.boolean().optional() }, | |
| async ({ filename, title, subtitle, author, date, sections }) => { | |
| const PdfPrinter = await getPdfMake(); | |
| const printer = new PdfPrinter(getPdfMakeFonts()); | |
| const content = [ | |
| { text:title, fontSize:24, bold:true, alignment:'center', margin:[0,50,0,10] }, | |
| subtitle ? { text:subtitle, fontSize:14, alignment:'center', color:'#666', margin:[0,0,0,5] } : null, | |
| author ? { text:`By: ${author}`, alignment:'center', color:'#666', margin:[0,0,0,5] } : null, | |
| { text:date||new Date().toLocaleDateString(), alignment:'center', color:'#666', margin:[0,0,0,30] }, | |
| { text:'', pageBreak:'after' }, | |
| ...(sections||[]).flatMap(s => [ | |
| { text:s.heading, fontSize:14, bold:true, margin:[0,15,0,5], color:'#2C3E50' }, | |
| { text:s.body, margin:[0,0,0,10] } | |
| ]) | |
| ].filter(Boolean); | |
| const docDef = { pageSize:'A4', content, defaultStyle:{ fontSize:11 }, footer:(pg,total) => ({ text:`Page ${pg} of ${total}`, alignment:'center', fontSize:9, color:'#999', margin:[0,10,0,0] }) }; | |
| const out = savePath(filename); | |
| await new Promise((res2,rej) => { const doc=printer.createPdfKitDocument(docDef); const st=fs.createWriteStream(out); doc.pipe(st); doc.end(); st.on('finish',res2); st.on('error',rej); }); | |
| return { content:[{ type:'text', text:`✅ Report তৈরি! (${sections.length} sections)\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('create_certificate', | |
| 'সুন্দর certificate PDF তৈরি করে। Decorative border, recipient name, achievement, issuer signature সহ।', | |
| { filename:z.string(), recipientName:z.string(), title:z.string().optional(), description:z.string().optional(), issuerName:z.string(), issuerTitle:z.string().optional(), date:z.string().optional(), certificateId:z.string().optional() }, | |
| async ({ filename, recipientName, title, description, issuerName, issuerTitle, date, certificateId }) => { | |
| const PdfPrinter = await getPdfMake(); | |
| const printer = new PdfPrinter(getPdfMakeFonts()); | |
| const docDef = { | |
| pageSize:'A4', pageOrientation:'landscape', | |
| content:[ | |
| { text:title||'Certificate of Achievement', fontSize:28, bold:true, alignment:'center', color:'#2C3E50', margin:[0,60,0,20] }, | |
| { text:'This is to certify that', fontSize:14, alignment:'center', color:'#555', margin:[0,0,0,10] }, | |
| { text:recipientName, fontSize:34, bold:true, alignment:'center', color:'#C9A227', margin:[0,5,0,10] }, | |
| { text:description||'has successfully completed the requirements', fontSize:12, alignment:'center', margin:[0,10,0,40] }, | |
| { columns:[ { text:`${issuerName}\n${issuerTitle||''}`, alignment:'center', bold:true }, { text:date||new Date().toLocaleDateString(), alignment:'center' } ] }, | |
| certificateId ? { text:`Certificate ID: ${certificateId}`, fontSize:8, color:'#999', alignment:'center', margin:[0,20,0,0] } : null, | |
| ].filter(Boolean) | |
| }; | |
| const out = savePath(filename); | |
| await new Promise((res2,rej) => { const doc=printer.createPdfKitDocument(docDef); const st=fs.createWriteStream(out); doc.pipe(st); doc.end(); st.on('finish',res2); st.on('error',rej); }); | |
| return { content:[{ type:'text', text:`✅ Certificate তৈরি! Recipient: ${recipientName}\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('create_resume', | |
| 'Professional CV/Resume PDF তৈরি করে। Contact info, summary, experience, education, skills sections সহ ATS-friendly format।', | |
| { filename:z.string(), personalInfo:z.object({ name:z.string(), email:z.string().optional(), phone:z.string().optional(), address:z.string().optional(), linkedin:z.string().optional(), github:z.string().optional() }), summary:z.string().optional(), experience:z.array(z.object({ company:z.string(), position:z.string(), duration:z.string(), description:z.string().optional() })).optional(), education:z.array(z.object({ institution:z.string(), degree:z.string(), year:z.string() })).optional(), skills:z.array(z.object({ category:z.string(), items:z.array(z.string()) })).optional() }, | |
| async ({ filename, personalInfo, summary, experience, education, skills }) => { | |
| const PdfPrinter = await getPdfMake(); | |
| const printer = new PdfPrinter(getPdfMakeFonts()); | |
| const p = personalInfo; | |
| const content = [ | |
| { text:p.name, fontSize:24, bold:true, alignment:'center' }, | |
| { text:[p.email,p.phone,p.address].filter(Boolean).join(' | '), alignment:'center', color:'#555', margin:[0,4,0,15] }, | |
| summary ? [{ text:'SUMMARY', fontSize:12, bold:true, margin:[0,8,0,4], decoration:'underline' }, { text:summary, margin:[0,0,0,10] }] : null, | |
| (experience||[]).length>0 ? { text:'EXPERIENCE', fontSize:12, bold:true, margin:[0,8,0,4], decoration:'underline' } : null, | |
| ...(experience||[]).map(e=>[ | |
| { columns:[{ text:e.position, bold:true },{ text:e.duration, alignment:'right', color:'#555' }] }, | |
| { text:e.company, color:'#2980B9' }, | |
| e.description ? { text:e.description, margin:[0,4,0,8] } : null | |
| ]).flat().filter(Boolean), | |
| (education||[]).length>0 ? { text:'EDUCATION', fontSize:12, bold:true, margin:[0,8,0,4], decoration:'underline' } : null, | |
| ...(education||[]).map(e=>[ { text:e.degree, bold:true }, { text:`${e.institution} — ${e.year}`, color:'#555', margin:[0,2,0,8] } ]).flat(), | |
| (skills||[]).length>0 ? { text:'SKILLS', fontSize:12, bold:true, margin:[0,8,0,4], decoration:'underline' } : null, | |
| ...(skills||[]).map(s=>({ text:`${s.category}: ${s.items.join(', ')}`, margin:[0,2,0,4] })), | |
| ].flat().filter(Boolean); | |
| const docDef = { pageSize:'A4', content, defaultStyle:{ fontSize:11 } }; | |
| const out = savePath(filename); | |
| await new Promise((res2,rej) => { const doc=printer.createPdfKitDocument(docDef); const st=fs.createWriteStream(out); doc.pipe(st); doc.end(); st.on('finish',res2); st.on('error',rej); }); | |
| return { content:[{ type:'text', text:`✅ Resume তৈরি! ${p.name}\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('create_letter', | |
| 'Formal letter/application PDF তৈরি করে। Sender, recipient, subject, body, closing সহ official letterhead format।', | |
| { filename:z.string(), senderName:z.string(), senderAddress:z.string().optional(), recipientName:z.string(), recipientOrg:z.string().optional(), recipientAddress:z.string().optional(), date:z.string().optional(), subject:z.string(), body:z.string(), closing:z.string().optional() }, | |
| async ({ filename, senderName, senderAddress, recipientName, recipientOrg, recipientAddress, date, subject, body, closing='Sincerely' }) => { | |
| const PdfPrinter = await getPdfMake(); | |
| const printer = new PdfPrinter(getPdfMakeFonts()); | |
| const docDef = { | |
| pageSize:'A4', | |
| content:[ | |
| { text:senderName, bold:true, fontSize:14 }, | |
| senderAddress ? { text:senderAddress, color:'#555' } : null, | |
| { text:date||new Date().toLocaleDateString(), margin:[0,10,0,15] }, | |
| { text:recipientName, bold:true }, | |
| recipientOrg ? { text:recipientOrg } : null, | |
| recipientAddress ? { text:recipientAddress, margin:[0,0,0,15] } : null, | |
| { text:`Subject: ${subject}`, bold:true, margin:[0,10,0,15] }, | |
| { text:'Dear '+recipientName+',', margin:[0,0,0,10] }, | |
| ...body.split('\n').map(p=>({ text:p, margin:[0,0,0,8] })), | |
| { text:closing+',', margin:[0,20,0,5] }, | |
| { text:senderName, bold:true }, | |
| ].filter(Boolean), | |
| defaultStyle:{ fontSize:11 } | |
| }; | |
| const out = savePath(filename); | |
| await new Promise((res2,rej) => { const doc=printer.createPdfKitDocument(docDef); const st=fs.createWriteStream(out); doc.pipe(st); doc.end(); st.on('finish',res2); st.on('error',rej); }); | |
| return { content:[{ type:'text', text:`✅ Letter তৈরি!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('create_booklet', | |
| 'Multi-section booklet/brochure PDF তৈরি করে। Cover page, chapters, page numbers সহ। Manual, catalog, magazine format।', | |
| { filename:z.string(), title:z.string(), subtitle:z.string().optional(), chapters:z.array(z.object({ title:z.string(), content:z.string() })), pageSize:z.string().optional() }, | |
| async ({ filename, title, subtitle, chapters, pageSize }) => { | |
| const PdfPrinter = await getPdfMake(); | |
| const printer = new PdfPrinter(getPdfMakeFonts()); | |
| const content = [ | |
| { text:title, fontSize:28, bold:true, alignment:'center', margin:[0,80,0,10] }, | |
| subtitle ? { text:subtitle, fontSize:14, alignment:'center', color:'#666' } : null, | |
| { text:'', pageBreak:'after' }, | |
| ...(chapters||[]).flatMap((c,i)=>[ | |
| { text:`${i+1}. ${c.title}`, fontSize:16, bold:true, margin:[0,20,0,10], color:'#2C3E50', pageBreak:i>0?'before':undefined }, | |
| { text:c.content, margin:[0,0,0,10] } | |
| ]) | |
| ].filter(Boolean); | |
| const docDef = { pageSize:pageSize||'A4', content, defaultStyle:{ fontSize:11 }, footer:(pg,total)=>({ text:`${pg} / ${total}`, alignment:'center', fontSize:9, color:'#999' }) }; | |
| const out = savePath(filename); | |
| await new Promise((res2,rej) => { const doc=printer.createPdfKitDocument(docDef); const st=fs.createWriteStream(out); doc.pipe(st); doc.end(); st.on('finish',res2); st.on('error',rej); }); | |
| return { content:[{ type:'text', text:`✅ Booklet তৈরি! (${chapters.length} chapters)\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('html_to_pdf', | |
| 'HTML string থেকে pixel-perfect PDF তৈরি করে। CSS styling, custom fonts সহ। Complex dashboard, styled reports সব কাজে।', | |
| { filename:z.string(), html:z.string(), pageSize:z.string().optional(), orientation:z.string().optional(), printBackground:z.boolean().optional() }, | |
| async ({ filename, html, pageSize, orientation, printBackground }) => { | |
| const puppeteer = await getPuppeteer(); | |
| const browser = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox'] }); | |
| const page = await browser.newPage(); | |
| await page.setContent(html, { waitUntil:'networkidle0' }); | |
| const out = savePath(filename); | |
| await page.pdf({ path:out, format:pageSize||'A4', landscape:orientation==='landscape', printBackground:printBackground!==false, margin:{top:'20px',bottom:'20px',left:'20px',right:'20px'} }); | |
| await browser.close(); | |
| return { content:[{ type:'text', text:`✅ HTML → PDF!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('markdown_to_pdf', | |
| 'Markdown text থেকে styled PDF তৈরি করে। Headings, bold, italic, lists, code blocks, tables সব support করে।', | |
| { filename:z.string(), markdown:z.string(), pageSize:z.string().optional() }, | |
| async ({ filename, markdown, pageSize }) => { | |
| const puppeteer = await getPuppeteer(); | |
| let html = markdown | |
| .replace(/^### (.+)$/gm,'<h3>$1</h3>') | |
| .replace(/^## (.+)$/gm,'<h2>$1</h2>') | |
| .replace(/^# (.+)$/gm,'<h1>$1</h1>') | |
| .replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>') | |
| .replace(/\*(.+?)\*/g,'<em>$1</em>') | |
| .replace(/`(.+?)`/g,'<code>$1</code>') | |
| .replace(/^\- (.+)$/gm,'<li>$1</li>') | |
| .replace(/\n\n/g,'</p><p>'); | |
| const fullHtml = `<html><head><style>body{font-family:Arial,sans-serif;max-width:800px;margin:40px auto;line-height:1.6;}h1,h2,h3{color:#2C3E50;}code{background:#f4f4f4;padding:2px 6px;border-radius:3px;}li{margin:4px 0;}</style></head><body><p>${html}</p></body></html>`; | |
| const browser = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox'] }); | |
| const page = await browser.newPage(); | |
| await page.setContent(fullHtml,{waitUntil:'networkidle0'}); | |
| const out = savePath(filename); | |
| await page.pdf({ path:out, format:pageSize||'A4', printBackground:true, margin:{top:'30px',bottom:'30px',left:'40px',right:'40px'} }); | |
| await browser.close(); | |
| return { content:[{ type:'text', text:`✅ Markdown → PDF!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| // ════════════════════════════════════════════════ | |
| // GROUP 2: pdf-lib — Edit + Merge + Security (15) | |
| // ════════════════════════════════════════════════ | |
| server.tool('merge_pdfs', | |
| 'একাধিক PDF ফাইল একটিতে merge করে। URL বা path থেকে PDF নিয়ে order অনুযায়ী combine করে।', | |
| { filename:z.string(), pdfUrls:z.array(z.string()) }, | |
| async ({ filename, pdfUrls }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const merged = await pdfLib.PDFDocument.create(); | |
| for (const url of pdfUrls) { | |
| const bytes = await fetchBytes(url); | |
| const doc = await pdfLib.PDFDocument.load(bytes); | |
| const pages = await merged.copyPages(doc, doc.getPageIndices()); | |
| pages.forEach(p => merged.addPage(p)); | |
| } | |
| const bytes = await merged.save(); | |
| fs.writeFileSync(savePath(filename), bytes); | |
| return { content:[{ type:'text', text:`✅ ${pdfUrls.length}টি PDF merge হয়েছে!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('split_pdf', | |
| 'PDF থেকে নির্দিষ্ট page ranges আলাদা করে। ranges না দিলে প্রতিটি page আলাদা PDF হয়।', | |
| { pdfUrl:z.string(), outputPrefix:z.string(), ranges:z.array(z.object({ start:z.number(), end:z.number(), name:z.string().optional() })).optional() }, | |
| async ({ pdfUrl, outputPrefix, ranges }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const src = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl)); | |
| const total = src.getPageCount(); | |
| const results = []; | |
| const rng = ranges||[]; | |
| if (rng.length===0) { | |
| for (let i=0;i<total;i++) { | |
| const d = await pdfLib.PDFDocument.create(); | |
| const [p] = await d.copyPages(src,[i]); | |
| d.addPage(p); | |
| const fname = `${outputPrefix}_page${i+1}`; | |
| fs.writeFileSync(savePath(fname), await d.save()); | |
| results.push(dlUrl(fname)); | |
| } | |
| } else { | |
| for (const r of rng) { | |
| const d = await pdfLib.PDFDocument.create(); | |
| const idx = []; | |
| for (let i=(r.start||1)-1;i<Math.min(r.end||total,total);i++) idx.push(i); | |
| const pages = await d.copyPages(src,idx); | |
| pages.forEach(p=>d.addPage(p)); | |
| const fname = r.name||`${outputPrefix}_${r.start}-${r.end}`; | |
| fs.writeFileSync(savePath(fname), await d.save()); | |
| results.push(dlUrl(fname)); | |
| } | |
| } | |
| return { content:[{ type:'text', text:`✅ ${results.length}টি ফাইল তৈরি:\n${results.map((r,i)=>`${i+1}. ${r}`).join('\n')}` }] }; | |
| } | |
| ); | |
| server.tool('extract_pages', | |
| 'PDF থেকে নির্দিষ্ট page নম্বর গুলো বের করে নতুন PDF তৈরি করে।', | |
| { pdfUrl:z.string(), filename:z.string(), pages:z.array(z.number()) }, | |
| async ({ pdfUrl, filename, pages }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const src = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl)); | |
| const d = await pdfLib.PDFDocument.create(); | |
| const ps = await d.copyPages(src, pages.map(p=>p-1)); | |
| ps.forEach(p=>d.addPage(p)); | |
| fs.writeFileSync(savePath(filename), await d.save()); | |
| return { content:[{ type:'text', text:`✅ ${pages.length}টি page extract হয়েছে!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('rotate_pages', | |
| 'PDF-এর page rotate করে। 90, 180, 270 degree। সব page বা নির্দিষ্ট page।', | |
| { pdfUrl:z.string(), filename:z.string(), rotation:z.number(), pages:z.array(z.number()).optional() }, | |
| async ({ pdfUrl, filename, rotation, pages }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl)); | |
| const idx = pages?.length>0 ? pages.map(p=>p-1) : doc.getPageIndices(); | |
| idx.forEach(i => doc.getPage(i).setRotation(pdfLib.degrees(rotation))); | |
| fs.writeFileSync(savePath(filename), await doc.save()); | |
| return { content:[{ type:'text', text:`✅ ${idx.length}টি page ${rotation}° rotate হয়েছে!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('add_watermark', | |
| 'PDF-এর প্রতিটি page-এ text watermark যোগ করে। Opacity, color, rotation, font size customize করা যায়।', | |
| { pdfUrl:z.string(), filename:z.string(), text:z.string(), opacity:z.number().optional(), fontSize:z.number().optional(), color:z.string().optional(), rotation:z.number().optional() }, | |
| async ({ pdfUrl, filename, text, opacity=0.3, fontSize=48, color='#FF0000', rotation=-45 }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl)); | |
| const font = await doc.embedFont(pdfLib.StandardFonts.HelveticaBold); | |
| const c = hexToRgb(color); | |
| for (const page of doc.getPages()) { | |
| const { width, height } = page.getSize(); | |
| page.drawText(text, { x:width/4, y:height/2, size:fontSize, font, color:pdfLib.rgb(c.r,c.g,c.b), opacity, rotate:pdfLib.degrees(rotation) }); | |
| } | |
| fs.writeFileSync(savePath(filename), await doc.save()); | |
| return { content:[{ type:'text', text:`✅ Watermark "${text}" যোগ হয়েছে!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('add_text_overlay', | |
| 'Existing PDF-এর নির্দিষ্ট position-এ text যোগ করে। Page, x, y, font size, color সব customize করা যায়।', | |
| { pdfUrl:z.string(), filename:z.string(), overlays:z.array(z.object({ page:z.number().optional(), text:z.string(), x:z.number(), y:z.number(), size:z.number().optional(), color:z.string().optional() })) }, | |
| async ({ pdfUrl, filename, overlays }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl)); | |
| const font = await doc.embedFont(pdfLib.StandardFonts.Helvetica); | |
| for (const o of overlays) { | |
| const c = hexToRgb(o.color||'#000000'); | |
| doc.getPage((o.page||1)-1).drawText(o.text, { x:o.x, y:o.y, size:o.size||12, font, color:pdfLib.rgb(c.r,c.g,c.b) }); | |
| } | |
| fs.writeFileSync(savePath(filename), await doc.save()); | |
| return { content:[{ type:'text', text:`✅ Text overlay যোগ হয়েছে!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('fill_pdf_form', | |
| 'Existing PDF form-এর fields fill করে। Text fields, checkboxes, radio buttons, dropdowns সব fill করা যায়।', | |
| { pdfUrl:z.string(), filename:z.string(), fields:z.record(z.any()), flatten:z.boolean().optional() }, | |
| async ({ pdfUrl, filename, fields, flatten }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl)); | |
| const form = doc.getForm(); | |
| for (const [name, value] of Object.entries(fields)) { | |
| try { | |
| const field = form.getField(name); | |
| const t = field.constructor.name; | |
| if (t.includes('Text')) field.setText(String(value)); | |
| else if (t.includes('CheckBox')) value ? field.check() : field.uncheck(); | |
| else if (t.includes('Dropdown')) field.select(String(value)); | |
| else if (t.includes('RadioGroup')) field.select(String(value)); | |
| } catch {} | |
| } | |
| if (flatten) form.flatten(); | |
| fs.writeFileSync(savePath(filename), await doc.save()); | |
| return { content:[{ type:'text', text:`✅ Form fill হয়েছে! ${Object.keys(fields).length}টি field\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('encrypt_pdf', | |
| 'PDF-এ password protection দেয়। User password ও owner password আলাদা। Print/copy/edit permission control।', | |
| { pdfUrl:z.string(), filename:z.string(), userPassword:z.string(), ownerPassword:z.string().optional() }, | |
| async ({ pdfUrl, filename, userPassword, ownerPassword }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl)); | |
| const bytes = await doc.save({ userPassword, ownerPassword:ownerPassword||userPassword+'_owner', permissions:{ printing:'none', modifying:false, copying:false, annotating:false } }); | |
| fs.writeFileSync(savePath(filename), bytes); | |
| return { content:[{ type:'text', text:`✅ PDF encrypt হয়েছে! Password protected.\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('decrypt_pdf', | |
| 'Password-protected PDF থেকে password remove করে। Owner password দিলে protection সরে যাবে।', | |
| { pdfUrl:z.string(), filename:z.string(), password:z.string() }, | |
| async ({ pdfUrl, filename, password }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl), { password }); | |
| fs.writeFileSync(savePath(filename), await doc.save()); | |
| return { content:[{ type:'text', text:`✅ PDF decrypt হয়েছে!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('add_page_numbers', | |
| 'PDF-এর প্রতিটি page-এ page number যোগ করে। Position, format, font size, color customize করা যায়।', | |
| { pdfUrl:z.string(), filename:z.string(), position:z.string().optional(), format:z.string().optional(), startNumber:z.number().optional(), fontSize:z.number().optional(), skipFirstPage:z.boolean().optional() }, | |
| async ({ pdfUrl, filename, position='bottom-center', format='simple', startNumber=1, fontSize=10, skipFirstPage }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl)); | |
| const font = await doc.embedFont(pdfLib.StandardFonts.Helvetica); | |
| const total = doc.getPageCount(); | |
| doc.getPages().forEach((page,i) => { | |
| if (skipFirstPage && i===0) return; | |
| const { width, height } = page.getSize(); | |
| const num = i+startNumber; | |
| const text = format==='full' ? `Page ${num} of ${total}` : String(num); | |
| let x=width/2-10, y=20; | |
| if (position.includes('top')) y=height-20; | |
| if (position.includes('left')) x=30; | |
| if (position.includes('right')) x=width-40; | |
| page.drawText(text, { x, y, size:fontSize, font, color:pdfLib.rgb(0,0,0) }); | |
| }); | |
| fs.writeFileSync(savePath(filename), await doc.save()); | |
| return { content:[{ type:'text', text:`✅ Page numbers যোগ হয়েছে!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('add_header_footer', | |
| 'PDF-এর প্রতিটি page-এ header ও footer যোগ করে। Left, center, right তিনটি column support।', | |
| { pdfUrl:z.string(), filename:z.string(), header:z.object({ left:z.string().optional(), center:z.string().optional(), right:z.string().optional() }).optional(), footer:z.object({ left:z.string().optional(), center:z.string().optional(), right:z.string().optional() }).optional() }, | |
| async ({ pdfUrl, filename, header, footer }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl)); | |
| const font = await doc.embedFont(pdfLib.StandardFonts.Helvetica); | |
| const gray = pdfLib.rgb(0.4,0.4,0.4); | |
| for (const page of doc.getPages()) { | |
| const { width, height } = page.getSize(); | |
| const draw = (section, y) => { | |
| if (!section) return; | |
| if (section.left) page.drawText(section.left, { x:30, y, size:9, font, color:gray }); | |
| if (section.center) page.drawText(section.center, { x:width/2-50, y, size:9, font, color:gray }); | |
| if (section.right) page.drawText(section.right, { x:width-100, y, size:9, font, color:gray }); | |
| }; | |
| draw(header, height-20); | |
| draw(footer, 10); | |
| } | |
| fs.writeFileSync(savePath(filename), await doc.save()); | |
| return { content:[{ type:'text', text:`✅ Header/Footer যোগ হয়েছে!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('compress_pdf', | |
| 'PDF file size কমায়। Object streams optimize করে file ছোট করে। Email বা upload-এর জন্য।', | |
| { pdfUrl:z.string(), filename:z.string() }, | |
| async ({ pdfUrl, filename }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const bytes = await fetchBytes(pdfUrl); | |
| const doc = await pdfLib.PDFDocument.load(bytes); | |
| const newBytes = await doc.save({ useObjectStreams:true }); | |
| fs.writeFileSync(savePath(filename), newBytes); | |
| const reduction = (((bytes.length-newBytes.length)/bytes.length)*100).toFixed(1); | |
| return { content:[{ type:'text', text:`✅ Compressed!\n📦 ${(bytes.length/1024).toFixed(1)}KB → ${(newBytes.length/1024).toFixed(1)}KB (${reduction}% reduction)\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('set_pdf_metadata', | |
| 'PDF-এর metadata update করে। Title, author, subject, keywords, creator সব modify করা যায়।', | |
| { pdfUrl:z.string(), filename:z.string(), title:z.string().optional(), author:z.string().optional(), subject:z.string().optional(), keywords:z.string().optional(), creator:z.string().optional() }, | |
| async ({ pdfUrl, filename, title, author, subject, keywords, creator }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl)); | |
| if (title) doc.setTitle(title); | |
| if (author) doc.setAuthor(author); | |
| if (subject) doc.setSubject(subject); | |
| if (keywords) doc.setKeywords(keywords.split(',').map(k=>k.trim())); | |
| if (creator) doc.setCreator(creator); | |
| fs.writeFileSync(savePath(filename), await doc.save()); | |
| return { content:[{ type:'text', text:`✅ Metadata update হয়েছে!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('add_digital_signature', | |
| 'PDF-এ visible digital signature box যোগ করে। Signer name, designation, organization, date সহ।', | |
| { pdfUrl:z.string(), filename:z.string(), signerName:z.string(), designation:z.string().optional(), organization:z.string().optional(), date:z.string().optional(), page:z.number().optional() }, | |
| async ({ pdfUrl, filename, signerName, designation, organization, date, page=1 }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl)); | |
| const font = await doc.embedFont(pdfLib.StandardFonts.HelveticaBold); | |
| const pg = doc.getPage(page-1); | |
| const { height } = pg.getSize(); | |
| pg.drawRectangle({ x:50, y:50, width:200, height:70, borderColor:pdfLib.rgb(0,0,0), borderWidth:1 }); | |
| pg.drawText(signerName, { x:55, y:95, size:12, font, color:pdfLib.rgb(0,0,0) }); | |
| if (designation) pg.drawText(designation, { x:55, y:80, size:9, font }); | |
| if (organization) pg.drawText(organization, { x:55, y:67, size:9, font }); | |
| pg.drawText(date||new Date().toLocaleDateString(), { x:55, y:55, size:8, font }); | |
| fs.writeFileSync(savePath(filename), await doc.save()); | |
| return { content:[{ type:'text', text:`✅ Signature যোগ হয়েছে! Signed by: ${signerName}\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| // ════════════════════════════════════════════ | |
| // GROUP 3: puppeteer (3 more tools) | |
| // ════════════════════════════════════════════ | |
| server.tool('url_to_pdf', | |
| 'যেকোনো website URL থেকে PDF তৈরি করে। Live webpage ভিজিট করে PDF বানায়।', | |
| { filename:z.string(), url:z.string(), pageSize:z.string().optional(), orientation:z.string().optional(), waitFor:z.number().optional() }, | |
| async ({ filename, url, pageSize, orientation, waitFor }) => { | |
| const puppeteer = await getPuppeteer(); | |
| const browser = await puppeteer.launch({ args:['--no-sandbox','--disable-setuid-sandbox'] }); | |
| const page = await browser.newPage(); | |
| await page.goto(url, { waitUntil:'networkidle2', timeout:30000 }); | |
| await new Promise(r=>setTimeout(r, waitFor||2000)); | |
| const out = savePath(filename); | |
| await page.pdf({ path:out, format:pageSize||'A4', landscape:orientation==='landscape', printBackground:true }); | |
| await browser.close(); | |
| return { content:[{ type:'text', text:`✅ "${url}" → PDF!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('generate_chart_pdf', | |
| 'Data থেকে chart সহ PDF তৈরি করে। Bar, line, pie, doughnut chart support। Multiple charts একই PDF-এ।', | |
| { filename:z.string(), title:z.string().optional(), charts:z.array(z.object({ type:z.string(), title:z.string().optional(), labels:z.array(z.string()), datasets:z.array(z.object({ label:z.string().optional(), data:z.array(z.number()), backgroundColor:z.any().optional() })) })), pageOrientation:z.string().optional() }, | |
| async ({ filename, title, charts, pageOrientation }) => { | |
| const { ChartJSNodeCanvas } = await import('chartjs-node-canvas'); | |
| const puppeteer = await getPuppeteer(); | |
| const canvas = new ChartJSNodeCanvas({ width:700, height:350 }); | |
| let html = `<html><head><style>body{font-family:Arial;padding:20px;}h1{color:#2C3E50;}.chart{margin:20px 0;}</style></head><body>`; | |
| if (title) html += `<h1>${title}</h1>`; | |
| for (const chart of charts) { | |
| const buf = await canvas.renderToBuffer({ type:chart.type||'bar', data:{ labels:chart.labels, datasets:chart.datasets }, options:{ responsive:false, plugins:{ title:{ display:!!chart.title, text:chart.title } } } }); | |
| html += `<div class="chart"><img src="data:image/png;base64,${buf.toString('base64')}" style="max-width:100%;"/></div>`; | |
| } | |
| html += '</body></html>'; | |
| const browser = await puppeteer.launch({ args:['--no-sandbox'] }); | |
| const page = await browser.newPage(); | |
| await page.setContent(html); | |
| const out = savePath(filename); | |
| await page.pdf({ path:out, format:'A4', landscape:pageOrientation==='landscape', printBackground:true }); | |
| await browser.close(); | |
| return { content:[{ type:'text', text:`✅ ${charts.length}টি chart সহ PDF!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| server.tool('html_template_to_pdf', | |
| 'Handlebars template ও data দিয়ে PDF তৈরি করে। {{variable}} syntax দিয়ে dynamic data inject করা যায়।', | |
| { filename:z.string(), template:z.string(), data:z.record(z.any()), pageSize:z.string().optional() }, | |
| async ({ filename, template, data, pageSize }) => { | |
| const Handlebars = (await import('handlebars')).default; | |
| const puppeteer = await getPuppeteer(); | |
| const compiled = Handlebars.compile(template); | |
| const html = compiled(data); | |
| const browser = await puppeteer.launch({ args:['--no-sandbox'] }); | |
| const page = await browser.newPage(); | |
| await page.setContent(html, { waitUntil:'networkidle0' }); | |
| const out = savePath(filename); | |
| await page.pdf({ path:out, format:pageSize||'A4', printBackground:true }); | |
| await browser.close(); | |
| return { content:[{ type:'text', text:`✅ Template → PDF!\n🔗 ${dlUrl(filename)}` }] }; | |
| } | |
| ); | |
| // ════════════════════════════════════════════ | |
| // GROUP 4: pdf-parse — Read + Extract (12) | |
| // ════════════════════════════════════════════ | |
| server.tool('extract_text', | |
| 'PDF থেকে সম্পূর্ণ text content বের করে। AI analysis, search indexing, translation-এর জন্য PDF content readable করে।', | |
| { pdfUrl:z.string(), includePageMarkers:z.boolean().optional() }, | |
| async ({ pdfUrl, includePageMarkers=true }) => { | |
| const pdfParse = await getPdfParse(); | |
| const bytes = await fetchBytes(pdfUrl); | |
| const data = await pdfParse(bytes); | |
| const text = includePageMarkers ? `[Pages: ${data.numpages}]\n\n${data.text}` : data.text; | |
| return { content:[{ type:'text', text:`✅ Text extracted!\n📄 Pages: ${data.numpages} | Chars: ${data.text.length}\n\n${text.substring(0,8000)}${text.length>8000?'\n[truncated...]':''}` }] }; | |
| } | |
| ); | |
| server.tool('extract_metadata', | |
| 'PDF-এর সম্পূর্ণ metadata বের করে। Title, author, subject, keywords, creation date, page count সব।', | |
| { pdfUrl:z.string() }, | |
| async ({ pdfUrl }) => { | |
| const pdfParse = await getPdfParse(); | |
| const data = await pdfParse(await fetchBytes(pdfUrl)); | |
| const m = data.info||{}; | |
| return { content:[{ type:'text', text:`✅ Metadata:\n📌 Title: ${m.Title||'N/A'}\n👤 Author: ${m.Author||'N/A'}\n📋 Subject: ${m.Subject||'N/A'}\n🏷️ Keywords: ${m.Keywords||'N/A'}\n📅 Created: ${m.CreationDate||'N/A'}\n📄 Pages: ${data.numpages}\n📊 PDF Version: ${m.PDFFormatVersion||'N/A'}` }] }; | |
| } | |
| ); | |
| server.tool('get_pdf_info', | |
| 'PDF সম্পর্কে বিস্তারিত technical information। Page count, size, encryption status, fonts, file size সব।', | |
| { pdfUrl:z.string() }, | |
| async ({ pdfUrl }) => { | |
| const pdfParse = await getPdfParse(); | |
| const bytes = await fetchBytes(pdfUrl); | |
| const data = await pdfParse(bytes); | |
| const { pdfLib } = await getPdfLib(); | |
| const doc = await pdfLib.PDFDocument.load(bytes, { ignoreEncryption:true }); | |
| const pages = doc.getPages(); | |
| const sizes = pages.slice(0,3).map((p,i)=>{ const s=p.getSize(); return ` Page ${i+1}: ${s.width.toFixed(0)}×${s.height.toFixed(0)} pts`; }); | |
| return { content:[{ type:'text', text:`✅ PDF Info:\n📄 Pages: ${doc.getPageCount()}\n📦 Size: ${(bytes.length/1024).toFixed(1)} KB\n📊 Version: ${data.info?.PDFFormatVersion||'N/A'}\n📐 Page sizes:\n${sizes.join('\n')}` }] }; | |
| } | |
| ); | |
| server.tool('search_in_pdf', | |
| 'PDF-এর ভেতরে text search করে। Keyword কোন page-এ কতবার আছে, context সহ দেখায়।', | |
| { pdfUrl:z.string(), query:z.string(), caseSensitive:z.boolean().optional(), contextChars:z.number().optional() }, | |
| async ({ pdfUrl, query, caseSensitive, contextChars=100 }) => { | |
| const pdfParse = await getPdfParse(); | |
| const data = await pdfParse(await fetchBytes(pdfUrl)); | |
| const text = data.text; | |
| const flags = caseSensitive?'g':'gi'; | |
| const pattern = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), flags); | |
| const matches = []; | |
| let m; | |
| while ((m=pattern.exec(text))!==null && matches.length<15) { | |
| const start = Math.max(0,m.index-contextChars); | |
| const end = Math.min(text.length,m.index+query.length+contextChars); | |
| matches.push(`...${text.substring(start,end)}...`); | |
| } | |
| return { content:[{ type:'text', text: matches.length>0 ? `✅ "${query}" — ${matches.length}টি match:\n\n${matches.join('\n---\n')}` : `❌ "${query}" পাওয়া যায়নি।` }] }; | |
| } | |
| ); | |
| server.tool('extract_form_data', | |
| 'Filled PDF form-এর সব field value বের করে JSON format-এ। Government forms, applications সব থেকে data extract।', | |
| { pdfUrl:z.string() }, | |
| async ({ pdfUrl }) => { | |
| const { pdfLib } = await getPdfLib(); | |
| const doc = await pdfLib.PDFDocument.load(await fetchBytes(pdfUrl), { ignoreEncryption:true }); | |
| let form; | |
| try { form = doc.getForm(); } catch { return { content:[{ type:'text', text:'❌ এই PDF-এ কোনো form নেই।' }] }; } | |
| const fields = {}; | |
| for (const field of form.getFields()) { | |
| const name = field.getName(); | |
| const type = field.constructor.name; | |
| let value = null; | |
| try { | |
| if (type.includes('Text')) value = field.getText(); | |
| else if (type.includes('CheckBox')) value = field.isChecked(); | |
| else if (type.includes('Dropdown')) value = field.getSelected(); | |
| else if (type.includes('RadioGroup')) value = field.getSelected(); | |
| } catch {} | |
| if (value!==null) fields[name] = { type, value }; | |
| } | |
| return { content:[{ type:'text', text:`✅ Form data:\n\n${JSON.stringify(fields,null,2)}` }] }; | |
| } | |
| ); | |
| server.tool('count_words', | |
| 'PDF-এর word count, character count, sentence count, estimated reading time বের করে।', | |
| { pdfUrl:z.string() }, | |
| async ({ pdfUrl }) => { | |
| const pdfParse = await getPdfParse(); | |
| const data = await pdfParse(await fetchBytes(pdfUrl)); | |
| const text = data.text; | |
| const words = text.split(/\s+/).filter(Boolean).length; | |
| const chars = text.replace(/\s/g,'').length; | |
| const sentences = text.split(/[.!?]+/).filter(Boolean).length; | |
| const readTime = Math.ceil(words/200); | |
| return { content:[{ type:'text', text:`✅ Word Count:\n📝 Words: ${words.toLocaleString()}\n🔡 Characters: ${chars.toLocaleString()}\n📖 Sentences: ${sentences.toLocaleString()}\n📄 Pages: ${data.numpages}\n⏱️ Reading time: ~${readTime} min` }] }; | |
| } | |
| ); | |
| server.tool('compare_pdfs', | |
| 'দুটো PDF ফাইলের text পার্থক্য বের করে। কোন অংশ যোগ বা বাদ হয়েছে দেখায়। Document version comparison।', | |
| { pdfUrl1:z.string(), pdfUrl2:z.string() }, | |
| async ({ pdfUrl1, pdfUrl2 }) => { | |
| const pdfParse = await getPdfParse(); | |
| const [d1,d2] = await Promise.all([pdfParse(await fetchBytes(pdfUrl1)), pdfParse(await fetchBytes(pdfUrl2))]); | |
| const l1 = new Set(d1.text.split('\n').filter(l=>l.trim())); | |
| const l2 = new Set(d2.text.split('\n').filter(l=>l.trim())); | |
| const added = [...l2].filter(l=>!l1.has(l)).slice(0,10); | |
| const removed = [...l1].filter(l=>!l2.has(l)).slice(0,10); | |
| return { content:[{ type:'text', text:`✅ Comparison:\n📄 PDF1: ${d1.numpages} pages\n📄 PDF2: ${d2.numpages} pages\n\n➕ Added:\n${added.map(l=>`+ ${l}`).join('\n')||'None'}\n\n➖ Removed:\n${removed.map(l=>`- ${l}`).join('\n')||'None'}` }] }; | |
| } | |
| ); | |
| server.tool('validate_pdf', | |
| 'PDF ফাইলটি valid কিনা check করে। Corrupted, encrypted, missing structure সব detect করে।', | |
| { pdfUrl:z.string() }, | |
| async ({ pdfUrl }) => { | |
| const bytes = await fetchBytes(pdfUrl); | |
| const results = []; | |
| results.push(`📦 Size: ${(bytes.length/1024).toFixed(1)} KB`); | |
| results.push(`🔍 Header: ${bytes.toString('ascii',0,8)}`); | |
| results.push(bytes.toString('ascii',0,5)==='%PDF-' ? '✅ Valid PDF header' : '❌ Invalid PDF header'); | |
| try { | |
| const pdfParse = await getPdfParse(); | |
| const data = await pdfParse(bytes); | |
| results.push(`✅ Parseable: ${data.numpages} pages`); | |
| } catch(e) { results.push(`❌ Parse error: ${e.message}`); } | |
| try { | |
| const { pdfLib } = await getPdfLib(); | |
| await pdfLib.PDFDocument.load(bytes, { ignoreEncryption:true }); | |
| results.push('✅ pdf-lib: Loadable'); | |
| if (bytes.toString('utf8',0,1024).includes('/Encrypt')) results.push('🔒 Encrypted: Yes'); | |
| else results.push('🔓 Encrypted: No'); | |
| } catch(e) { results.push(`⚠️ pdf-lib: ${e.message}`); } | |
| return { content:[{ type:'text', text:`✅ Validation:\n\n${results.join('\n')}` }] }; | |
| } | |
| ); | |
| server.tool('pdf_summary', | |
| 'PDF-এর content পড়ে structured summary তৈরি করে। Title, author, page count, key sections, reading time সহ।', | |
| { pdfUrl:z.string(), maxWords:z.number().optional() }, | |
| async ({ pdfUrl, maxWords=300 }) => { | |
| const pdfParse = await getPdfParse(); | |
| const bytes = await fetchBytes(pdfUrl); | |
| const data = await pdfParse(bytes); | |
| const text = data.text; | |
| const words = text.split(/\s+/).filter(Boolean); | |
| const headings = text.match(/^[A-Z][^.!?\n]{5,50}$/gm)?.slice(0,5)||[]; | |
| return { content:[{ type:'text', text:`✅ Summary:\n📌 Title: ${data.info?.Title||'Unknown'}\n👤 Author: ${data.info?.Author||'Unknown'}\n📄 Pages: ${data.numpages}\n📝 Words: ${words.length.toLocaleString()}\n⏱️ Reading: ~${Math.ceil(words.length/200)} min\n\n📋 Key Sections:\n${headings.map(h=>`• ${h.trim()}`).join('\n')||'• (Not detected)'}\n\n📖 Preview:\n${words.slice(0,maxWords).join(' ')}${words.length>maxWords?'...':''}` }] }; | |
| } | |
| ); | |
| server.tool('extract_tables', | |
| 'PDF-এ থাকা table গুলো detect করে structured data হিসেবে বের করে। Financial reports, data tables থেকে extract করে।', | |
| { pdfUrl:z.string() }, | |
| async ({ pdfUrl }) => { | |
| const pdfParse = await getPdfParse(); | |
| const data = await pdfParse(await fetchBytes(pdfUrl)); | |
| const lines = data.text.split('\n').filter(l=>l.trim()); | |
| const tables = []; | |
| let current = []; | |
| for (const line of lines) { | |
| const cols = line.split(/\s{2,}|\t/).filter(Boolean); | |
| if (cols.length>=2) current.push(cols.join(' | ')); | |
| else if (current.length>1) { tables.push([...current]); current=[]; } | |
| } | |
| if (current.length>1) tables.push(current); | |
| return { content:[{ type:'text', text: tables.length>0 ? `✅ ${tables.length}টি table found:\n\n${tables.map((t,i)=>`Table ${i+1}:\n${t.join('\n')}`).join('\n\n')}` : '❌ No tables detected.' }] }; | |
| } | |
| ); | |
| server.tool('pdf_to_images', | |
| 'PDF-এর প্রতিটি page কে image-এ convert করে। PNG format, custom DPI। Puppeteer দিয়ে render করে।', | |
| { pdfUrl:z.string(), outputPrefix:z.string(), pages:z.array(z.number()).optional() }, | |
| async ({ pdfUrl, outputPrefix, pages }) => { | |
| const pdfParse = await getPdfParse(); | |
| const puppeteer = await getPuppeteer(); | |
| const bytes = await fetchBytes(pdfUrl); | |
| const data = await pdfParse(bytes); | |
| const totalPages = data.numpages; | |
| const pageList = pages?.length>0 ? pages : Array.from({length:totalPages},(_,i)=>i+1); | |
| const results = []; | |
| const b64 = bytes.toString('base64'); | |
| const browser = await puppeteer.launch({ args:['--no-sandbox'] }); | |
| for (const pageNum of pageList.slice(0,5)) { | |
| const page = await browser.newPage(); | |
| await page.goto(`data:application/pdf;base64,${b64}`,{waitUntil:'networkidle0'}); | |
| const imgPath = path.join(DOWNLOADS_DIR,`${outputPrefix}_page${pageNum}.png`); | |
| await page.screenshot({ path:imgPath, fullPage:true }); | |
| results.push(`${BASE_URL}/download/${outputPrefix}_page${pageNum}.png`); | |
| await page.close(); | |
| } | |
| await browser.close(); | |
| return { content:[{ type:'text', text:`✅ ${results.length}টি page image হয়েছে:\n${results.join('\n')}` }] }; | |
| } | |
| ); | |
| server.tool('list_downloads', | |
| 'Server-এ থাকা সব generated PDF ফাইলের list দেখায়। File name, size, creation time এবং download URL সহ।', | |
| { filter:z.string().optional() }, | |
| async ({ filter }) => { | |
| const files = fs.readdirSync(DOWNLOADS_DIR) | |
| .filter(f=>filter?f.includes(filter):true) | |
| .map(f=>{ const s=fs.statSync(path.join(DOWNLOADS_DIR,f)); return `📄 ${f}\n 💾 ${(s.size/1024).toFixed(1)}KB | 🔗 ${BASE_URL}/download/${f}`; }); | |
| return { content:[{ type:'text', text: files.length>0 ? `📁 ${files.length}টি ফাইল:\n\n${files.join('\n\n')}` : '📂 কোনো ফাইল নেই।' }] }; | |
| } | |
| ); | |
| // ── Connect ──────────────────────────────────────────────── | |
| const transport = new StreamableHTTPServerTransport({ sessionIdGenerator:undefined }); | |
| await server.connect(transport); | |
| await transport.handleRequest(req, res, req.body); | |
| }); | |
| app.listen(PORT, () => { | |
| console.log(`\n✅ PDF MCP Server started!`); | |
| console.log(`📌 MCP URL : ${BASE_URL}/mcp`); | |
| console.log(`📁 Files : ${BASE_URL}/files`); | |
| console.log(`⬇️ Download : ${BASE_URL}/download/<filename>`); | |
| console.log(`🛠️ Tools : 40\n`); | |
| }); | |