Skip to main content
Glama
mcp.ts12.1 kB
/** * Day 5 - Remote MCP API Endpoint * Vercel Serverless Function for Claude Web Custom Connectors */ import { VercelRequest, VercelResponse } from '@vercel/node'; import { z } from 'zod'; import { docs_v1, drive_v3 } from 'googleapis'; import { GoogleAuth } from 'google-auth-library'; // CORS headers for Claude Web const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Max-Age': '86400', }; // MCP Request/Response schemas const MCPToolCallSchema = z.object({ method: z.literal('tools/call'), params: z.object({ name: z.string(), arguments: z.record(z.any()).optional() }) }); const MCPListToolsSchema = z.object({ method: z.literal('tools/list'), params: z.object({}).optional() }); // Tool argument schemas (from Day4) const CreateDocumentSchema = z.object({ title: z.string().min(1).max(200).describe("Title of document to create") }); const WriteToDocumentSchema = z.object({ documentId: z.string().describe("Document ID to write to"), content: z.string().max(10000).describe("Text content to insert"), insertIndex: z.number().optional().describe("Insert position (default: 1)") }); const ListDocumentsSchema = z.object({ maxResults: z.number().min(1).max(50).optional().describe("Max documents (default: 10)") }); // Document interface interface DocumentInfo { id: string; title: string; createdTime: string; modifiedTime: string; webViewLink: string; } // EpisodicRAG Google Docs Service (optimized for Vercel) class VercelEpisodicRAGService { private docsApi: docs_v1.Docs; private driveApi: drive_v3.Drive; private episodicRAGFolderId: string | null = null; constructor() { // Service Account authentication from environment const auth = new GoogleAuth({ credentials: this.getServiceAccountCredentials(), scopes: [ 'https://www.googleapis.com/auth/documents', 'https://www.googleapis.com/auth/drive' ] }); this.docsApi = new docs_v1.Docs({ auth }); this.driveApi = new drive_v3.Drive({ auth }); } private getServiceAccountCredentials() { const serviceAccountKey = process.env.GOOGLE_SERVICE_ACCOUNT_KEY; if (!serviceAccountKey) { throw new Error('GOOGLE_SERVICE_ACCOUNT_KEY environment variable required'); } try { return JSON.parse(serviceAccountKey); } catch (error) { throw new Error('Invalid GOOGLE_SERVICE_ACCOUNT_KEY format'); } } private async getEpisodicRAGFolder(): Promise<string> { if (this.episodicRAGFolderId) { return this.episodicRAGFolderId; } const searchResponse = await this.driveApi.files.list({ q: "name='EpisodicRAG' and mimeType='application/vnd.google-apps.folder' and trashed=false", fields: 'files(id,name)' }); if (searchResponse.data.files && searchResponse.data.files.length > 0) { this.episodicRAGFolderId = searchResponse.data.files[0].id!; return this.episodicRAGFolderId; } // Create folder if not exists const createResponse = await this.driveApi.files.create({ requestBody: { name: 'EpisodicRAG', mimeType: 'application/vnd.google-apps.folder' }, fields: 'id' }); this.episodicRAGFolderId = createResponse.data.id!; return this.episodicRAGFolderId; } async createDocument(title: string): Promise<DocumentInfo> { const folderId = await this.getEpisodicRAGFolder(); const response = await this.driveApi.files.create({ requestBody: { name: title, mimeType: 'application/vnd.google-apps.document', parents: [folderId] }, fields: 'id,name,createdTime,modifiedTime,webViewLink' }); const file = response.data; return { id: file.id!, title: file.name!, createdTime: file.createdTime!, modifiedTime: file.modifiedTime!, webViewLink: file.webViewLink! }; } async writeToDocument(documentId: string, content: string, insertIndex: number = 1): Promise<void> { await this.docsApi.documents.batchUpdate({ documentId, requestBody: { requests: [ { insertText: { location: { index: insertIndex }, text: content } } ] } }); } async listLoopDocuments(maxResults: number = 10): Promise<DocumentInfo[]> { const folderId = await this.getEpisodicRAGFolder(); const response = await this.driveApi.files.list({ q: `'${folderId}' in parents and mimeType='application/vnd.google-apps.document' and name contains 'Loop' and trashed=false`, orderBy: 'modifiedTime desc', pageSize: maxResults, fields: 'files(id,name,createdTime,modifiedTime,webViewLink)' }); return response.data.files?.map(file => ({ id: file.id!, title: file.name!, createdTime: file.createdTime!, modifiedTime: file.modifiedTime!, webViewLink: file.webViewLink! })) || []; } async createLearningLogDocument(): Promise<DocumentInfo> { const title = `Remote Loop Learning Day1-5 - ${new Date().toLocaleDateString()}`; const document = await this.createDocument(title); const content = `EpisodicRAG Remote Learning Record Generated: ${new Date().toLocaleString()} Source: Claude Web via Remote MCP Location: EpisodicRAG Folder === Day1-4 Foundation === Day1: Python basics and JSON operations Day2: Local MCP server and tool creation Day3: Google API authentication systems Day4: OAuth2 + Local MCP integration === Day5: Remote MCP Revolution === - Local MCP → Remote MCP Server transformation - Vercel serverless deployment - Claude Web Custom Connectors integration - Universal access architecture (Desktop + Web) Technical Achievement: - Vercel function: Request/Response MCP protocol - Service Account: Server-side Google API authentication - EpisodicRAG: Centralized knowledge management - 10-second optimization: Efficient API operations This demonstrates the complete evolution from local Python functions to scalable remote API integration, all within the EpisodicRAG knowledge framework. Generated by Day5 Remote MCP - Vercel Serverless Function`; await this.writeToDocument(document.id, content); return document; } } // Main handler function export default async function handler(req: VercelRequest, res: VercelResponse) { // Apply CORS headers for ALL requests (including OPTIONS) Object.entries(corsHeaders).forEach(([key, value]) => { res.setHeader(key, value); }); // Handle CORS preflight if (req.method === 'OPTIONS') { return res.status(200).json({}).end(); } try { // Parse MCP request const body = req.body; // Handle tools/list if (body?.method === 'tools/list') { // Return proper MCP protocol response const tools = [ { name: "create_document", description: "Create a new Google Docs document in EpisodicRAG folder", inputSchema: { type: "object", properties: { title: { type: "string", description: "Title of the document to create", minLength: 1, maxLength: 200 } }, required: ["title"] } }, { name: "write_to_document", description: "Write text to a Google Docs document", inputSchema: { type: "object", properties: { documentId: { type: "string", description: "Document ID to write to" }, content: { type: "string", description: "Text content to insert", maxLength: 10000 }, insertIndex: { type: "number", description: "Insert position index (default: 1)" } }, required: ["documentId", "content"] } }, { name: "list_loop_documents", description: "List Loop documents in EpisodicRAG folder", inputSchema: { type: "object", properties: { maxResults: { type: "number", description: "Maximum number of documents (default: 10)", minimum: 1, maximum: 50 } }, required: [] } }, { name: "create_learning_log", description: "Auto-generate Day1-5 learning record in EpisodicRAG", inputSchema: { type: "object", properties: {}, required: [] } } ]; return res.status(200).json({ tools }); } // Handle tools/call if (body?.method === 'tools/call') { const { name, arguments: args } = MCPToolCallSchema.parse(body).params; const service = new VercelEpisodicRAGService(); switch (name) { case "create_document": { const { title } = CreateDocumentSchema.parse(args); const document = await service.createDocument(title); return res.status(200).json({ content: [ { type: "text", text: `Document created in EpisodicRAG folder via Remote MCP!\n\nTitle: ${document.title}\nID: ${document.id}\nURL: ${document.webViewLink}\nCreated: ${new Date(document.createdTime).toLocaleString()}\n\nSource: Claude Web → Vercel → Google Docs API` } ] }); } case "write_to_document": { const { documentId, content, insertIndex } = WriteToDocumentSchema.parse(args); await service.writeToDocument(documentId, content, insertIndex); return res.status(200).json({ content: [ { type: "text", text: `Text written to EpisodicRAG document via Remote MCP!\n\nDocument ID: ${documentId}\nContent Length: ${content.length} characters\nURL: https://docs.google.com/document/d/${documentId}/edit` } ] }); } case "list_loop_documents": { const { maxResults } = ListDocumentsSchema.parse(args || {}); const documents = await service.listLoopDocuments(maxResults); const documentsList = documents.map((doc, index) => `${index + 1}. ${doc.title}\n ID: ${doc.id}\n Modified: ${new Date(doc.modifiedTime).toLocaleString()}\n URL: ${doc.webViewLink}` ).join('\n\n'); return res.status(200).json({ content: [ { type: "text", text: `Loop Documents in EpisodicRAG (Remote MCP) - ${documents.length} found:\n\n${documentsList || 'No Loop documents found.'}` } ] }); } case "create_learning_log": { const document = await service.createLearningLogDocument(); return res.status(200).json({ content: [ { type: "text", text: `Day1-5 Learning Record created via Remote MCP!\n\nTitle: ${document.title}\nURL: ${document.webViewLink}\n\nThis record documents the complete journey from Day1 Python to Day5 Remote MCP, generated by Claude Web through Vercel serverless function!` } ] }); } default: return res.status(400).json({ error: `Unknown tool: ${name}` }); } } // Invalid request return res.status(400).json({ error: "Invalid MCP request format" }); } catch (error) { console.error('MCP Handler Error:', error); return res.status(500).json({ error: error instanceof Error ? error.message : 'Internal server error', timestamp: new Date().toISOString() }); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Bizuayeu/day5-api-remote-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server