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
450 lines (439 loc) • 16.1 kB
JavaScript
import Database from 'better-sqlite3';
import * as path from 'path';
import * as fs from 'fs';
class EnhancedDatabaseManager {
db = null;
dbPath;
constructor() {
// Use gdrive_db directory in the repository root
const dbDir = path.join(process.cwd(), 'gdrive_db');
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
this.dbPath = path.join(dbDir, 'gdrive-index-v2.db');
}
ensureConnection() {
if (!this.db) {
this.db = new Database(this.dbPath);
this.db.pragma('journal_mode = WAL');
this.db.pragma('foreign_keys = ON');
this.db.pragma('busy_timeout = 5000');
this.initializeSchema();
}
return this.db;
}
initializeSchema() {
const db = this.db;
db.exec(`
-- Base documents table
CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY,
drive_id TEXT UNIQUE NOT NULL,
drive_path TEXT,
name TEXT NOT NULL,
mime_type TEXT,
size INTEGER,
created_time TEXT,
modified_time TEXT,
indexed_time TEXT DEFAULT CURRENT_TIMESTAMP,
ai_model TEXT,
ai_processing_time_ms INTEGER,
doc_type TEXT CHECK(doc_type IN ('contract', 'invoice', 'proposal', 'project_plan', 'report', 'unknown')),
synopsis TEXT,
confidence_score REAL CHECK(confidence_score >= 0 AND confidence_score <= 1)
);
-- Contracts table
CREATE TABLE IF NOT EXISTS contracts (
id INTEGER PRIMARY KEY,
document_id INTEGER NOT NULL UNIQUE,
contract_number TEXT,
contract_type TEXT,
effective_date DATE,
expiration_date DATE,
auto_renewal BOOLEAN,
notice_period_days INTEGER,
total_value DECIMAL(15,2),
currency TEXT,
payment_terms TEXT,
governing_law TEXT,
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
);
-- Contract parties
CREATE TABLE IF NOT EXISTS contract_parties (
contract_id INTEGER,
party_name TEXT,
party_role TEXT CHECK(party_role IN ('buyer', 'seller', 'vendor', 'client', 'other')),
PRIMARY KEY (contract_id, party_name),
FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE
);
-- Invoices table
CREATE TABLE IF NOT EXISTS invoices (
id INTEGER PRIMARY KEY,
document_id INTEGER NOT NULL UNIQUE,
invoice_number TEXT,
invoice_date DATE,
due_date DATE,
supplier_name TEXT,
supplier_address TEXT,
customer_name TEXT,
customer_address TEXT,
subtotal DECIMAL(15,2),
tax_amount DECIMAL(15,2),
total_amount DECIMAL(15,2),
currency TEXT,
payment_status TEXT CHECK(payment_status IN ('paid', 'unpaid', 'partial', 'overdue')),
payment_method TEXT,
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
);
-- Invoice line items
CREATE TABLE IF NOT EXISTS invoice_items (
id INTEGER PRIMARY KEY,
invoice_id INTEGER,
description TEXT,
quantity DECIMAL(10,3),
unit_price DECIMAL(15,2),
amount DECIMAL(15,2),
FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE
);
-- Proposals table
CREATE TABLE IF NOT EXISTS proposals (
id INTEGER PRIMARY KEY,
document_id INTEGER NOT NULL UNIQUE,
proposal_number TEXT,
client_name TEXT,
project_name TEXT,
proposal_date DATE,
valid_until DATE,
total_value DECIMAL(15,2),
currency TEXT,
timeline_days INTEGER,
status TEXT,
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
);
-- Proposal deliverables
CREATE TABLE IF NOT EXISTS proposal_deliverables (
id INTEGER PRIMARY KEY,
proposal_id INTEGER,
description TEXT,
timeline TEXT,
value DECIMAL(15,2),
FOREIGN KEY (proposal_id) REFERENCES proposals(id) ON DELETE CASCADE
);
-- Sync state
CREATE TABLE IF NOT EXISTS sync_state (
id INTEGER PRIMARY KEY DEFAULT 1,
last_sync_time TEXT,
drive_change_token TEXT,
total_files_indexed INTEGER DEFAULT 0,
total_errors INTEGER DEFAULT 0,
CHECK (id = 1)
);
-- Processing queue
CREATE TABLE IF NOT EXISTS processing_queue (
id INTEGER PRIMARY KEY,
drive_id TEXT NOT NULL,
operation TEXT CHECK(operation IN ('index', 'reindex', 'delete')),
priority INTEGER DEFAULT 5,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'processing', 'completed', 'failed')),
retry_count INTEGER DEFAULT 0,
error_message TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
processed_at TEXT
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_doc_type ON documents(doc_type);
CREATE INDEX IF NOT EXISTS idx_doc_modified ON documents(modified_time);
CREATE INDEX IF NOT EXISTS idx_doc_drive_id ON documents(drive_id);
CREATE INDEX IF NOT EXISTS idx_queue_status ON processing_queue(status, priority);
CREATE INDEX IF NOT EXISTS idx_contract_expiry ON contracts(expiration_date);
CREATE INDEX IF NOT EXISTS idx_invoice_due ON invoices(due_date);
CREATE INDEX IF NOT EXISTS idx_doc_name ON documents(name);
CREATE INDEX IF NOT EXISTS idx_doc_synopsis ON documents(synopsis);
-- Initialize sync state if not exists
INSERT OR IGNORE INTO sync_state (id) VALUES (1);
`);
}
// Transaction wrapper
async transaction(callback) {
const db = this.ensureConnection();
return db.transaction(callback)(db);
}
// Document operations
async insertDocument(doc) {
const db = this.ensureConnection();
const stmt = db.prepare(`
INSERT INTO documents (
drive_id, drive_path, name, mime_type, size,
created_time, modified_time, ai_model, ai_processing_time_ms,
doc_type, synopsis, confidence_score
) VALUES (
, , , , ,
, , , ,
, ,
)
`);
const result = stmt.run(doc);
return result.lastInsertRowid;
}
async upsertDocument(doc) {
const db = this.ensureConnection();
const stmt = db.prepare(`
INSERT INTO documents (
drive_id, drive_path, name, mime_type, size,
created_time, modified_time, ai_model, ai_processing_time_ms,
doc_type, synopsis, confidence_score
) VALUES (
, , , , ,
, , , ,
, ,
)
ON CONFLICT(drive_id) DO UPDATE SET
drive_path = excluded.drive_path,
name = excluded.name,
mime_type = excluded.mime_type,
size = excluded.size,
modified_time = excluded.modified_time,
ai_model = excluded.ai_model,
ai_processing_time_ms = excluded.ai_processing_time_ms,
doc_type = excluded.doc_type,
synopsis = excluded.synopsis,
confidence_score = excluded.confidence_score,
indexed_time = CURRENT_TIMESTAMP
RETURNING id
`);
const result = stmt.get(doc);
return result.id;
}
async getDocument(id) {
const db = this.ensureConnection();
const stmt = db.prepare('SELECT * FROM documents WHERE id = ?');
return stmt.get(id) || null;
}
async getDocumentByDriveId(driveId) {
const db = this.ensureConnection();
const stmt = db.prepare('SELECT * FROM documents WHERE drive_id = ?');
return stmt.get(driveId) || null;
}
// Contract operations
async insertContract(docId, contract) {
const db = this.ensureConnection();
const stmt = db.prepare(`
INSERT INTO contracts (
document_id, contract_number, contract_type, effective_date,
expiration_date, auto_renewal, notice_period_days, total_value,
currency, payment_terms, governing_law
) VALUES (
, , , ,
, , , ,
, ,
)
`);
const result = stmt.run({ document_id: docId, ...contract });
return result.lastInsertRowid;
}
async insertContractParties(contractId, parties) {
const db = this.ensureConnection();
const stmt = db.prepare(`
INSERT INTO contract_parties (contract_id, party_name, party_role)
VALUES (, , )
`);
for (const party of parties) {
stmt.run({ contract_id: contractId, ...party });
}
}
// Invoice operations
async insertInvoice(docId, invoice) {
const db = this.ensureConnection();
const stmt = db.prepare(`
INSERT INTO invoices (
document_id, invoice_number, invoice_date, due_date,
supplier_name, supplier_address, customer_name, customer_address,
subtotal, tax_amount, total_amount, currency,
payment_status, payment_method
) VALUES (
, , , ,
, , , ,
, , , ,
,
)
`);
const result = stmt.run({ document_id: docId, ...invoice });
return result.lastInsertRowid;
}
async insertInvoiceItems(invoiceId, items) {
const db = this.ensureConnection();
const stmt = db.prepare(`
INSERT INTO invoice_items (invoice_id, description, quantity, unit_price, amount)
VALUES (, , , , )
`);
for (const item of items) {
stmt.run({ invoice_id: invoiceId, ...item });
}
}
// Proposal operations
async insertProposal(docId, proposal) {
const db = this.ensureConnection();
const stmt = db.prepare(`
INSERT INTO proposals (
document_id, proposal_number, client_name, project_name,
proposal_date, valid_until, total_value, currency,
timeline_days, status
) VALUES (
, , , ,
, , , ,
,
)
`);
const result = stmt.run({ document_id: docId, ...proposal });
return result.lastInsertRowid;
}
// Queue operations
async addToProcessingQueue(items) {
const db = this.ensureConnection();
const stmt = db.prepare(`
INSERT INTO processing_queue (drive_id, operation, priority)
VALUES (, , )
`);
const insertMany = db.transaction((items) => {
for (const item of items) {
stmt.run(item);
}
});
insertMany(items);
}
async getNextQueueItem() {
const db = this.ensureConnection();
const stmt = db.prepare(`
SELECT * FROM processing_queue
WHERE status = 'pending'
ORDER BY priority DESC, created_at ASC
LIMIT 1
`);
return stmt.get() || null;
}
async updateQueueItemStatus(id, status, error) {
const db = this.ensureConnection();
const stmt = db.prepare(`
UPDATE processing_queue
SET status = ,
error_message = ,
processed_at = CASE WHEN IN ('completed', 'failed') THEN CURRENT_TIMESTAMP ELSE processed_at END
WHERE id =
`);
stmt.run({ id, status, error_message: error });
}
// Sync state operations
async getSyncState() {
const db = this.ensureConnection();
const stmt = db.prepare('SELECT * FROM sync_state WHERE id = 1');
return stmt.get();
}
async updateSyncState(updates) {
const db = this.ensureConnection();
const fields = Object.keys(updates).map(k => `${k} = {k}`).join(', ');
const stmt = db.prepare(`UPDATE sync_state SET ${fields} WHERE id = 1`);
stmt.run(updates);
}
// Advanced queries
async searchDocuments(query, filters) {
const db = this.ensureConnection();
let sql = `
SELECT d.* FROM documents d
WHERE (d.name LIKE OR d.synopsis LIKE )
`;
const params = { query: `%${query}%` };
if (filters?.doc_type) {
sql += ' AND d.doc_type = @doc_type';
params.doc_type = filters.doc_type;
}
if (filters?.date_from) {
sql += ' AND d.created_time >= @date_from';
params.date_from = filters.date_from;
}
if (filters?.date_to) {
sql += ' AND d.created_time <= @date_to';
params.date_to = filters.date_to;
}
sql += ' ORDER BY d.modified_time DESC LIMIT 100';
const stmt = db.prepare(sql);
return stmt.all(params);
}
async getExpiringContracts(days) {
const db = this.ensureConnection();
const stmt = db.prepare(`
SELECT d.*, c.*
FROM documents d
JOIN contracts c ON c.document_id = d.id
WHERE c.expiration_date BETWEEN date('now') AND date('now', '+' || ? || ' days')
ORDER BY c.expiration_date ASC
`);
return stmt.all(days);
}
async getOverdueInvoices() {
const db = this.ensureConnection();
const stmt = db.prepare(`
SELECT d.*, i.*
FROM documents d
JOIN invoices i ON i.document_id = d.id
WHERE i.due_date < date('now')
AND i.payment_status IN ('unpaid', 'partial')
ORDER BY i.due_date ASC
`);
return stmt.all();
}
async getStats() {
const db = this.ensureConnection();
const stats = db.prepare(`
SELECT
COUNT(*) as total_documents,
COUNT(CASE WHEN created_time > datetime('now', '-7 days') THEN 1 END) as documents_last_7_days
FROM documents
`).get();
const byType = db.prepare(`
SELECT doc_type, COUNT(*) as count
FROM documents
GROUP BY doc_type
`).all();
const contractValue = db.prepare(`
SELECT COALESCE(SUM(total_value), 0) as total
FROM contracts
`).get();
const unpaidInvoices = db.prepare(`
SELECT COALESCE(SUM(total_amount), 0) as total
FROM invoices
WHERE payment_status IN ('unpaid', 'partial')
`).get();
return {
total_documents: stats.total_documents,
by_type: Object.fromEntries(byType.map(r => [r.doc_type, r.count])),
total_contract_value: contractValue.total,
unpaid_invoices_value: unpaidInvoices.total,
documents_last_7_days: stats.documents_last_7_days
};
}
async resetDatabase() {
const db = this.ensureConnection();
// Drop all tables
db.exec(`
DROP TABLE IF EXISTS proposal_deliverables;
DROP TABLE IF EXISTS proposals;
DROP TABLE IF EXISTS invoice_items;
DROP TABLE IF EXISTS invoices;
DROP TABLE IF EXISTS contract_parties;
DROP TABLE IF EXISTS contracts;
DROP TABLE IF EXISTS processing_queue;
DROP TABLE IF EXISTS sync_state;
DROP TABLE IF EXISTS documents;
`);
// Reinitialize
this.initializeSchema();
}
close() {
if (this.db) {
this.db.close();
this.db = null;
}
}
}
// Export singleton instance
export const dbV2 = new EnhancedDatabaseManager();