mcp-gdrive-enhanced-markov
Version:
MCP server for Google Drive and Sheets with write capabilities, AI-powered PDF analysis, SQLite document indexing, and advanced directory exploration tools
226 lines (215 loc) • 9.29 kB
JavaScript
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/chat/completions';
const DOCUMENT_PARSER_PROMPT = `You are a document classifier and metadata extractor. Analyze the provided document and return a JSON response.
IMPORTANT: You MUST classify the document as one of exactly these 4 types:
- "invoice": Any billing document, invoice, or payment request
- "contract": Any agreement, contract, or legally binding document
- "proposal": Any proposal, quote, bid, or offer document
- "report": Any report, analysis, summary, or informational document
If the document doesn't perfectly fit one category, choose the CLOSEST match.
NEVER use any other value. Do NOT use "other" or any custom type.
Return JSON with this exact structure:
{
"doc_type": "invoice" | "contract" | "proposal" | "report",
"synopsis": "1-2 sentence summary of the document's content and purpose",
"fields": {
// Type-specific fields based on doc_type
}
}
For each document type, include these specific fields:
INVOICE:
- invoice_number: string | null
- invoice_date: string (ISO date) | null
- due_date: string (ISO date) | null
- currency: string (3-letter code) | null
- total: number | null
- tax: number | null
- supplier: string (company name) | null
- customer: string (company name) | null
CONTRACT:
- contract_number: string | null
- contract_type: string | null
- parties: string[] (list of party names)
- effective_date: string (ISO date) | null
- expiration_date: string (ISO date) | null
- value: number | null
- currency: string (3-letter code) | null
- key_terms: string[] (important terms/conditions)
PROPOSAL:
- proposal_number: string | null
- proposal_date: string (ISO date) | null
- client: string | null
- project_name: string | null
- total_value: number | null
- currency: string (3-letter code) | null
- timeline: string | null
- key_deliverables: string[]
REPORT:
- report_title: string | null
- report_date: string (ISO date) | null
- author: string | null
- report_type: string | null
- key_findings: string[]
- recommendations: string[]
Choose the most appropriate type from the 4 options above for any document.
If a field cannot be determined from the document, use null.
Ensure all dates are in ISO format (YYYY-MM-DD).
Ensure all monetary values are numbers without currency symbols.`;
export class AIDocumentParser {
apiKey;
constructor() {
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) {
throw new Error('OPENROUTER_API_KEY environment variable is not set');
}
this.apiKey = apiKey;
}
async parseDocument(fileContent, fileName, mimeType, options = {}) {
try {
// Convert PDF to base64 for multimodal analysis
const base64Content = fileContent.toString('base64');
const dataUri = `data:${mimeType};base64,${base64Content}`;
const model = options.useThinkingMode
? 'google/gemini-2.0-flash-thinking-exp-1219:free'
: 'google/gemini-2.5-flash-preview-05-20';
const response = await fetch(OPENROUTER_API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://github.com/markov/google-drive-mcp',
'X-Title': 'Google Drive MCP Document Indexer'
},
body: JSON.stringify({
model,
messages: [
{
role: 'system',
content: DOCUMENT_PARSER_PROMPT
},
{
role: 'user',
content: [
{
type: 'text',
text: `Analyze this document named "${fileName}" and extract the requested information.`
},
{
type: 'file',
file: {
filename: fileName,
file_data: dataUri
}
}
]
}
],
temperature: 0.1,
max_tokens: 2000,
response_format: {
type: 'json_schema',
json_schema: {
name: 'document_parse_result',
strict: true,
schema: {
type: 'object',
required: ['doc_type', 'synopsis', 'fields'],
properties: {
doc_type: {
type: 'string',
enum: ['invoice', 'contract', 'proposal', 'report']
},
synopsis: {
type: 'string',
description: '1-2 sentence summary of the document'
},
fields: {
type: 'object',
additionalProperties: true
}
},
additionalProperties: false
}
}
}
})
});
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
}
catch (e) {
errorData = await response.text();
}
console.error('OpenRouter API Error Details:', {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
error: errorData,
model: model,
fileSize: fileContent.length,
fileName: fileName
});
throw new Error(`OpenRouter API error: ${response.status} - ${JSON.stringify(errorData)}`);
}
const data = await response.json();
if (!data.choices || !data.choices[0] || !data.choices[0].message) {
throw new Error('Invalid response from OpenRouter API');
}
// Parse the JSON response
const content = data.choices[0].message.content;
const parsed = JSON.parse(content);
// Validate the response structure
if (!parsed.doc_type || !parsed.synopsis || !parsed.fields) {
throw new Error('Invalid document parse result structure');
}
// Additional validation: ensure doc_type is one of the allowed values
const allowedTypes = ['invoice', 'contract', 'proposal', 'report'];
if (!allowedTypes.includes(parsed.doc_type)) {
console.warn(`Invalid doc_type '${parsed.doc_type}' received, defaulting to 'report'`);
parsed.doc_type = 'report';
}
return parsed;
}
catch (error) {
// Return a fallback result on error
console.error('AI parsing error:', error);
return {
doc_type: 'report',
synopsis: `Failed to parse document: ${fileName}`,
fields: {
error: error instanceof Error ? error.message : 'Unknown error',
file_name: fileName
}
};
}
}
async parseWithRetry(fileContent, fileName, mimeType, options = {}, maxRetries = 3) {
let lastError = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await this.parseDocument(fileContent, fileName, mimeType, options);
}
catch (error) {
lastError = error instanceof Error ? error : new Error('Unknown error');
console.error(`Parse attempt ${attempt} failed:`, error);
if (attempt < maxRetries) {
// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// All retries failed, return fallback
return {
doc_type: 'report',
synopsis: `Failed to parse document after ${maxRetries} attempts: ${fileName}`,
fields: {
error: lastError?.message || 'Unknown error',
file_name: fileName
}
};
}
}
// Export singleton instance
export const aiParser = new AIDocumentParser();