Skip to main content
Glama
tools.ts43.2 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import fetch from 'node-fetch'; import { promises as fs } from 'fs'; import { exec } from 'child_process'; import { promisify } from 'util'; import { tmpdir } from 'os'; import { handleApiResponse, formatSuccessMessage } from './response-handler.js'; import { UsageTracker } from './usage-tracker.js'; import { CreateBucketRequest, createBucketRequestSchema, rawSQLRequestSchema, RawSQLRequest, FunctionUpdateRequest, functionUpdateRequestSchema, functionUploadRequestSchema, bulkUpsertRequestSchema, } from '@insforge/shared-schemas'; import FormData from 'form-data'; const execAsync = promisify(exec); /** * Configuration for the tools */ export interface ToolsConfig { apiKey?: string; apiBaseUrl?: string; } /** * Health check response from backend */ interface HealthCheckResponse { status: string; version: string; service: string; timestamp: string; } /** * Version cache entry */ interface VersionCacheEntry { version: string; timestamp: number; } /** * Tool version requirements map * Maps tool names to their minimum required backend version */ const TOOL_VERSION_REQUIREMENTS: Record<string, string> = { 'upsert-schedule': '1.1.1', // 'get-schedules': '1.1.1', // 'get-schedule-logs': '1.1.1', 'delete-schedule': '1.1.1', }; /** * Register all Insforge tools on an MCP server * This centralizes all tool definitions to avoid duplication */ export function registerInsforgeTools(server: McpServer, config: ToolsConfig = {}) { const GLOBAL_API_KEY = config.apiKey || process.env.API_KEY || ''; const API_BASE_URL = config.apiBaseUrl || process.env.API_BASE_URL || 'http://localhost:7130'; // Initialize usage tracker const usageTracker = new UsageTracker(API_BASE_URL, GLOBAL_API_KEY); // Version cache with 5-minute TTL let versionCache: VersionCacheEntry | null = null; const VERSION_CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds /** * Fetch backend version from health endpoint with caching */ async function getBackendVersion(): Promise<string> { const now = Date.now(); // Return cached version if still valid if (versionCache && (now - versionCache.timestamp) < VERSION_CACHE_TTL) { return versionCache.version; } try { const response = await fetch(`${API_BASE_URL}/api/health`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error(`Health check failed with status ${response.status}`); } const health: HealthCheckResponse = await response.json(); // Cache the version versionCache = { version: health.version, timestamp: now, }; return health.version; } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to fetch backend version: ${errMsg}`); } } /** * Compare semantic versions (e.g., "1.1.0" vs "1.0.0") * Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2 */ function compareVersions(v1: string, v2: string): number { // Strip 'v' prefix if present and remove pre-release metadata (e.g., "-dev.31") const clean1 = v1.replace(/^v/, '').split('-')[0]; const clean2 = v2.replace(/^v/, '').split('-')[0]; const parts1 = clean1.split('.').map(Number); const parts2 = clean2.split('.').map(Number); for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { const part1 = parts1[i] || 0; const part2 = parts2[i] || 0; if (part1 > part2) return 1; if (part1 < part2) return -1; } return 0; } /** * Check if tool is supported by current backend version * @throws Error if version requirement is not met */ async function checkToolVersion(toolName: string): Promise<void> { const requiredVersion = TOOL_VERSION_REQUIREMENTS[toolName]; // If no version requirement, tool is available in all versions if (!requiredVersion) { return; } try { const currentVersion = await getBackendVersion(); if (compareVersions(currentVersion, requiredVersion) < 0) { throw new Error( `Tool '${toolName}' requires backend version ${requiredVersion} or higher, but current version is ${currentVersion}. Please upgrade your Insforge backend server.` ); } } catch (error) { if (error instanceof Error && error.message.includes('requires backend version')) { throw error; // Re-throw version mismatch errors } // If health check fails, log warning but allow tool to proceed console.warn(`Warning: Could not verify backend version for tool '${toolName}': ${error instanceof Error ? error.message : 'Unknown error'}`); } } // Helper function to track tool usage async function trackToolUsage(toolName: string, success: boolean = true): Promise<void> { if (GLOBAL_API_KEY) { await usageTracker.trackUsage(toolName, success); } } // Wrapper function to add usage tracking to tools function withUsageTracking<T extends unknown[], R>( toolName: string, handler: (...args: T) => Promise<R> ): (...args: T) => Promise<R> { return async (...args: T): Promise<R> => { try { const result = await handler(...args); await trackToolUsage(toolName, true); return result; } catch (error) { await trackToolUsage(toolName, false); throw error; } }; } // Helper function to get API key - always uses global API key // The optional parameter is kept for backward compatibility but ignored const getApiKey = (_toolApiKey?: string): string => { if (!GLOBAL_API_KEY) { throw new Error('API key is required. Pass --api_key when starting the MCP server.'); } return GLOBAL_API_KEY; }; // Helper function to fetch documentation from backend const fetchDocumentation = async (docType: string): Promise<string> => { try { const response = await fetch(`${API_BASE_URL}/api/docs/${docType}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); // Check for 404 before processing response if (response.status === 404) { throw new Error('Documentation not found'); } const result = await handleApiResponse(response); if (result && typeof result === 'object' && 'content' in result) { let content = result.content; // Replace all example/placeholder URLs with actual API_BASE_URL // Handle URLs whether they're in backticks, quotes, or standalone // Preserve paths after the domain by only replacing the base URL content = content.replace(/http:\/\/localhost:7130/g, API_BASE_URL); content = content.replace(/https:\/\/your-app\.region\.insforge\.app/g, API_BASE_URL); return content; } throw new Error('Invalid response format from documentation endpoint'); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; throw new Error(`Unable to retrieve ${docType} documentation: ${errMsg}`); } }; // Helper function to fetch insforge-project.md content const fetchInsforgeInstructionsContext = async (): Promise<string | null> => { try { return await fetchDocumentation('instructions'); } catch (error) { console.error('Failed to fetch insforge-instructions.md:', error); return null; } }; // Helper function to add background context to responses // Only enabled for backend versions < 1.1.7 (legacy support) const addBackgroundContext = async <T extends { content: Array<{ type: 'text'; text: string }> }>(response: T): Promise<T> => { try { const currentVersion = await getBackendVersion(); const isLegacyVersion = compareVersions(currentVersion, '1.1.7') < 0; // Only add context for versions before 1.1.7 if (isLegacyVersion) { const context = await fetchInsforgeInstructionsContext(); if (context && response.content && Array.isArray(response.content)) { response.content.push({ type: 'text' as const, text: `\n\n---\n🔧 INSFORGE DEVELOPMENT RULES (Auto-loaded):\n${context}`, }); } } } catch { // If version check fails, skip background context (safer default) console.warn('Could not determine backend version, skipping background context'); } return response; }; // -------------------------------------------------- // INSTRUCTION TOOLS // -------------------------------------------------- server.tool( 'fetch-docs', 'Fetch Insforge documentation. Use "instructions" for essential backend setup (MANDATORY FIRST), or select specific SDK docs for database, auth, storage, functions, or AI integration.', { docType: z .enum(['instructions', 'db-sdk', 'storage-sdk', 'functions-sdk', 'ai-integration-sdk','auth-components-react']) .describe( 'Documentation type: "instructions" (essential backend setup - use FIRST), "db-sdk" (database operations), "storage-sdk" (file storage), "functions-sdk" (edge functions), "ai-integration-sdk" (AI features), "auth-components-react" (authentication components for React+Vite applications).' ), }, withUsageTracking('fetch-docs', async ({ docType }) => { try { const content = await fetchDocumentation(docType); return await addBackgroundContext({ content: [ { type: 'text', text: content, }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; // Friendly error for not found (likely due to old backend version) if (errMsg.includes('404') || errMsg.toLowerCase().includes('not found')) { return { content: [{ type: 'text' as const, text: `Documentation for "${docType}" is not available. This is likely because your backend version is too old and doesn't support this documentation endpoint yet. This won't affect the functionality of the tools - they will still work correctly.` }], }; } // Generic error response - no background context return { content: [{ type: 'text' as const, text: `Error fetching ${docType} documentation: ${errMsg}` }], }; } }) ); server.tool( 'get-anon-key', 'Generate an anonymous JWT token that never expires. Requires admin API key. Use this for client-side applications that need public access.', { apiKey: z .string() .optional() .describe('API key for authentication (optional if provided via --api_key)'), }, withUsageTracking('get-anon-key', async ({ apiKey }) => { try { const actualApiKey = getApiKey(apiKey); const response = await fetch(`${API_BASE_URL}/api/auth/tokens/anon`, { method: 'POST', headers: { 'x-api-key': actualApiKey, 'Content-Type': 'application/json', }, }); const result = await handleApiResponse(response); return await addBackgroundContext({ content: [ { type: 'text', text: formatSuccessMessage('Anonymous token generated', result), }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error generating anonymous token: ${errMsg}`, }, ], isError: true, }; } }) ); // -------------------------------------------------- // DATABASE TOOLS // -------------------------------------------------- server.tool( 'get-table-schema', 'Returns the detailed schema(including RLS, indexes, constraints, etc.) of a specific table', { apiKey: z .string() .optional() .describe('API key for authentication (optional if provided via --api_key)'), tableName: z.string().describe('Name of the table'), }, withUsageTracking('get-table-schema', async ({ apiKey, tableName }) => { try { const actualApiKey = getApiKey(apiKey); const response = await fetch(`${API_BASE_URL}/api/metadata/${tableName}`, { method: 'GET', headers: { 'x-api-key': actualApiKey, }, }); const result = await handleApiResponse(response); return await addBackgroundContext({ content: [ { type: 'text', text: formatSuccessMessage('Schema retrieved', result), }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error getting table schema: ${errMsg}`, }, ], isError: true, }; } }) ); server.tool( 'get-backend-metadata', 'Index all backend metadata', { apiKey: z .string() .optional() .describe('API key for authentication (optional if provided via --api_key)'), }, withUsageTracking('get-backend-metadata', async ({ apiKey }) => { try { const actualApiKey = getApiKey(apiKey); const response = await fetch(`${API_BASE_URL}/api/metadata?mcp=true`, { method: 'GET', headers: { 'x-api-key': actualApiKey, }, }); const metadata = await handleApiResponse(response); return await addBackgroundContext({ content: [ { type: 'text', text: `Backend metadata:\n\n${JSON.stringify(metadata, null, 2)}`, }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error retrieving backend metadata: ${errMsg}`, }, ], isError: true, }; } }) ); server.tool( 'run-raw-sql', 'Execute raw SQL query with optional parameters. Admin access required. Use with caution as it can modify data directly.', { apiKey: z .string() .optional() .describe('API key for authentication (optional if provided via --api_key)'), ...rawSQLRequestSchema.shape, }, withUsageTracking('run-raw-sql', async ({ apiKey, query, params }) => { try { const actualApiKey = getApiKey(apiKey); const requestBody: RawSQLRequest = { query, params: params || [], }; const response = await fetch(`${API_BASE_URL}/api/database/advance/rawsql`, { method: 'POST', headers: { 'x-api-key': actualApiKey, 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); const result = await handleApiResponse(response); return await addBackgroundContext({ content: [ { type: 'text', text: formatSuccessMessage('SQL query executed', result), }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error executing SQL query: ${errMsg}`, }, ], isError: true, }; } }) ); server.tool( 'download-template', 'CRITICAL: MANDATORY FIRST STEP for all new InsForge projects. Download pre-configured starter template (React) to a temporary directory. After download, you MUST copy files to current directory using the provided command.', { frame: z .enum(['react']) .describe('Framework to use for the template (currently only React is supported)'), projectName: z .string() .optional() .describe('Name for the project directory (optional, defaults to "insforge-react")'), }, withUsageTracking('download-template', async ({ frame, projectName }) => { try { // Get the anon key from backend const response = await fetch(`${API_BASE_URL}/api/auth/tokens/anon`, { method: 'POST', headers: { 'x-api-key': getApiKey(), 'Content-Type': 'application/json', }, }); const result = await handleApiResponse(response); const anonKey = result.accessToken; if (!anonKey) { throw new Error('Failed to retrieve anon key from backend'); } // Create temp directory for download const tempDir = tmpdir(); const targetDir = projectName || `insforge-${frame}`; const templatePath = `${tempDir}/${targetDir}`; console.error(`[download-template] Target path: ${templatePath}`); // Check if template already exists in temp, remove it first try { const stats = await fs.stat(templatePath); if (stats.isDirectory()) { console.error(`[download-template] Removing existing template at ${templatePath}`); await fs.rm(templatePath, { recursive: true, force: true }); } } catch { // Directory doesn't exist, which is fine } const command = `npx create-insforge-app ${targetDir} --frame ${frame} --base-url ${API_BASE_URL} --anon-key ${anonKey} --skip-install`; // Execute the npx command in temp directory const { stdout, stderr } = await execAsync(command, { maxBuffer: 10 * 1024 * 1024, // 10MB buffer cwd: tempDir, }); // Check if command was successful (basic validation) const output = stdout || stderr || ''; if (output.toLowerCase().includes('error') && !output.includes('successfully')) { throw new Error(`Failed to download template: ${output}`); } return await addBackgroundContext({ content: [ { type: 'text', text: `✅ React template downloaded successfully 📁 Template Location: ${templatePath} ⚠️ IMPORTANT: The template is in a temporary directory and NOT in your current working directory. 🔴 CRITICAL NEXT STEP REQUIRED: You MUST copy ALL files (INCLUDING HIDDEN FILES like .env, .gitignore, etc.) from the temporary directory to your current project directory. Copy all files from: ${templatePath} To: Your current project directory `, }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error downloading template: ${errMsg}`, }, ], isError: true, }; } }) ); server.tool( 'bulk-upsert', 'Bulk insert or update data from CSV or JSON file. Supports upsert operations with a unique key.', { apiKey: z .string() .optional() .describe('API key for authentication (optional if provided via --api_key)'), ...bulkUpsertRequestSchema.shape, filePath: z.string().describe('Path to CSV or JSON file containing data to import'), }, withUsageTracking('bulk-upsert', async ({ apiKey, table, filePath, upsertKey }) => { try { const actualApiKey = getApiKey(apiKey); // Read the file const fileBuffer = await fs.readFile(filePath); const fileName = filePath.split('/').pop() || 'data.csv'; // Create form data for multipart upload const formData = new FormData(); formData.append('file', fileBuffer, fileName); formData.append('table', table); if (upsertKey) { formData.append('upsertKey', upsertKey); } const response = await fetch(`${API_BASE_URL}/api/database/advance/bulk-upsert`, { method: 'POST', headers: { 'x-api-key': actualApiKey, ...formData.getHeaders(), }, body: formData, }); const result = await handleApiResponse(response); // Format the result message const message = result.success ? `Successfully processed ${result.rowsAffected} of ${result.totalRecords} records into table "${result.table}"` : result.message || 'Bulk upsert operation completed'; return await addBackgroundContext({ content: [ { type: 'text', text: formatSuccessMessage('Bulk upsert completed', { message, table: result.table, rowsAffected: result.rowsAffected, totalRecords: result.totalRecords, errors: result.errors, }), }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error performing bulk upsert: ${errMsg}`, }, ], isError: true, }; } }) ); // -------------------------------------------------- // STORAGE TOOLS // -------------------------------------------------- server.tool( 'create-bucket', 'Create new storage bucket', { apiKey: z .string() .optional() .describe('API key for authentication (optional if provided via --api_key)'), ...createBucketRequestSchema.shape, }, withUsageTracking('create-bucket', async ({ apiKey, bucketName, isPublic }) => { try { const actualApiKey = getApiKey(apiKey); const response = await fetch(`${API_BASE_URL}/api/storage/buckets`, { method: 'POST', headers: { 'x-api-key': actualApiKey, 'Content-Type': 'application/json', }, body: JSON.stringify({ bucketName, isPublic } as CreateBucketRequest), }); const result = await handleApiResponse(response); return await addBackgroundContext({ content: [ { type: 'text', text: formatSuccessMessage('Bucket created', result), }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error creating bucket: ${errMsg}`, }, ], isError: true, }; } }) ); server.tool( 'list-buckets', 'Lists all storage buckets', {}, withUsageTracking('list-buckets', async () => { try { const response = await fetch(`${API_BASE_URL}/api/storage/buckets`, { method: 'GET', headers: { 'x-api-key': getApiKey(), }, }); const result = await handleApiResponse(response); return await addBackgroundContext({ content: [ { type: 'text', text: formatSuccessMessage('Buckets retrieved', result), }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error listing buckets: ${errMsg}`, }, ], isError: true, }; } }) ); server.tool( 'delete-bucket', 'Deletes a storage bucket', { apiKey: z .string() .optional() .describe('API key for authentication (optional if provided via --api_key)'), bucketName: z.string().describe('Name of the bucket to delete'), }, withUsageTracking('delete-bucket', async ({ apiKey, bucketName }) => { try { const actualApiKey = getApiKey(apiKey); const response = await fetch(`${API_BASE_URL}/api/storage/buckets/${bucketName}`, { method: 'DELETE', headers: { 'x-api-key': actualApiKey, }, }); const result = await handleApiResponse(response); return await addBackgroundContext({ content: [ { type: 'text', text: formatSuccessMessage('Bucket deleted', result), }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error deleting bucket: ${errMsg}`, }, ], isError: true, }; } }) ); // -------------------------------------------------- // EDGE FUNCTION TOOLS // -------------------------------------------------- server.tool( 'create-function', 'Create a new edge function that runs in Deno runtime. The code must be written to a file first for version control', { ...functionUploadRequestSchema.omit({ code: true }).shape, codeFile: z .string() .describe( 'Path to JavaScript file containing the function code. Must export: module.exports = async function(request) { return new Response(...) }' ), }, withUsageTracking('create-function', async (args) => { try { let code: string; try { code = await fs.readFile(args.codeFile, 'utf-8'); } catch (fileError) { throw new Error( `Failed to read code file '${args.codeFile}': ${fileError instanceof Error ? fileError.message : 'Unknown error'}` ); } const response = await fetch(`${API_BASE_URL}/api/functions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': getApiKey(), }, body: JSON.stringify({ slug: args.slug, name: args.name, code: code, description: args.description, status: args.status, }), }); const result = await handleApiResponse(response); return await addBackgroundContext({ content: [ { type: 'text', text: formatSuccessMessage( `Edge function '${args.slug}' created successfully from ${args.codeFile}`, result ), }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error creating function: ${errMsg}`, }, ], isError: true, }; } }) ); server.tool( 'get-function', 'Get details of a specific edge function including its code', { slug: z.string().describe('The slug identifier of the function'), }, withUsageTracking('get-function', async (args) => { try { const response = await fetch(`${API_BASE_URL}/api/functions/${args.slug}`, { method: 'GET', headers: { 'x-api-key': getApiKey(), }, }); const result = await handleApiResponse(response); return await addBackgroundContext({ content: [ { type: 'text', text: formatSuccessMessage(`Edge function '${args.slug}' details`, result), }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error getting function: ${errMsg}`, }, ], isError: true, }; } }) ); server.tool( 'update-function', 'Update an existing edge function code or metadata', { slug: z.string().describe('The slug identifier of the function to update'), ...functionUpdateRequestSchema.omit({ code: true }).shape, codeFile: z .string() .optional() .describe( 'Path to JavaScript file containing the new function code. Must export: module.exports = async function(request) { return new Response(...) }' ), }, withUsageTracking('update-function', async (args) => { try { const updateData: FunctionUpdateRequest = {}; if (args.name) { updateData.name = args.name; } if (args.codeFile) { try { updateData.code = await fs.readFile(args.codeFile, 'utf-8'); } catch (fileError) { throw new Error( `Failed to read code file '${args.codeFile}': ${fileError instanceof Error ? fileError.message : 'Unknown error'}` ); } } if (args.description !== undefined) { updateData.description = args.description; } if (args.status) { updateData.status = args.status; } const response = await fetch(`${API_BASE_URL}/api/functions/${args.slug}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'x-api-key': getApiKey(), }, body: JSON.stringify(updateData), }); const result = await handleApiResponse(response); const fileInfo = args.codeFile ? ` from ${args.codeFile}` : ''; return await addBackgroundContext({ content: [ { type: 'text', text: formatSuccessMessage( `Edge function '${args.slug}' updated successfully${fileInfo}`, result ), }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error updating function: ${errMsg}`, }, ], isError: true, }; } }) ); server.tool( 'delete-function', 'Delete an edge function permanently', { slug: z.string().describe('The slug identifier of the function to delete'), }, withUsageTracking('delete-function', async (args) => { try { const response = await fetch(`${API_BASE_URL}/api/functions/${args.slug}`, { method: 'DELETE', headers: { 'x-api-key': getApiKey(), }, }); const result = await handleApiResponse(response); return await addBackgroundContext({ content: [ { type: 'text', text: formatSuccessMessage(`Edge function '${args.slug}' deleted successfully`, result), }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error deleting function: ${errMsg}`, }, ], isError: true, }; } }) ); // -------------------------------------------------- // CONTAINER LOGS TOOLS // -------------------------------------------------- server.tool( 'get-container-logs', 'Get latest logs from a specific container/service. Use this to help debug problems with your app.', { apiKey: z .string() .optional() .describe('API key for authentication (optional if provided via --api_key)'), source: z.enum(['insforge.logs', 'postgREST.logs', 'postgres.logs', 'function.logs']).describe('Log source to retrieve'), limit: z.number().optional().default(20).describe('Number of logs to return (default: 20)'), }, withUsageTracking('get-container-logs', async ({ apiKey, source, limit }) => { try { const actualApiKey = getApiKey(apiKey); const queryParams = new URLSearchParams(); if (limit) queryParams.append('limit', limit.toString()); let response = await fetch(`${API_BASE_URL}/api/logs/${source}?${queryParams}`, { method: 'GET', headers: { 'x-api-key': actualApiKey, }, }); // Fallback to legacy endpoint if 404 if (response.status === 404) { response = await fetch(`${API_BASE_URL}/api/logs/analytics/${source}?${queryParams}`, { method: 'GET', headers: { 'x-api-key': actualApiKey, }, }); } const result = await handleApiResponse(response); return await addBackgroundContext({ content: [ { type: 'text', text: formatSuccessMessage(`Latest logs from ${source}`, result), }, ], }); } catch (error) { const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [ { type: 'text', text: `Error retrieving container logs: ${errMsg}`, }, ], isError: true, }; } }) ); // -------------------------------------------------- // SCHEDULE TOOLS (CRON JOBS) - COMMENTED OUT // -------------------------------------------------- // server.tool( // 'upsert-schedule', // 'Create or update a cron job schedule. If id is provided, updates existing schedule; otherwise creates a new one.', // { // apiKey: z // .string() // .optional() // .describe('API key for authentication (optional if provided via --api_key)'), // id: z // .string() // .uuid() // .optional() // .describe('The UUID of the schedule to update. If omitted, a new schedule will be created.'), // name: z.string().min(3).describe('Schedule name (at least 3 characters)'), // cronSchedule: z // .string() // .describe('Cron schedule format (5 or 6 parts, e.g., "0 */2 * * *" for every 2 hours)'), // functionUrl: z.string().url().describe('The URL to call when the schedule triggers'), // httpMethod: z // .enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) // .optional() // .default('POST') // .describe('HTTP method to use'), // headers: z // .record(z.string()) // .optional() // .describe('HTTP headers. Values starting with "secret:" will be resolved from secrets store.'), // body: z // .record(z.unknown()) // .optional() // .describe('JSON body to send with the request'), // }, // withUsageTracking('upsert-schedule', async ({ apiKey, id, name, cronSchedule, functionUrl, httpMethod, headers, body }) => { // try { // // Check backend version compatibility // await checkToolVersion('upsert-schedule'); // const actualApiKey = getApiKey(apiKey); // const requestBody: any = { // name, // cronSchedule, // functionUrl, // httpMethod: httpMethod || 'POST', // }; // if (id) { // requestBody.id = id; // } // if (headers) { // requestBody.headers = headers; // } // if (body) { // requestBody.body = body; // } // const response = await fetch(`${API_BASE_URL}/api/schedules`, { // method: 'POST', // headers: { // 'x-api-key': actualApiKey, // 'Content-Type': 'application/json', // }, // body: JSON.stringify(requestBody), // }); // const result = await handleApiResponse(response); // const action = id ? 'updated' : 'created'; // return await addBackgroundContext({ // content: [ // { // type: 'text', // text: formatSuccessMessage(`Schedule '${name}' ${action} successfully`, result), // }, // ], // }); // } catch (error) { // const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; // return { // content: [ // { // type: 'text', // text: `Error upserting schedule: ${errMsg}`, // }, // ], // isError: true, // }; // } // }) // ); // server.tool( // 'get-schedules', // 'List all cron job schedules', // { // apiKey: z // .string() // .optional() // .describe('API key for authentication (optional if provided via --api_key)'), // scheduleId: z // .string() // .uuid() // .optional() // .describe('Optional: Get a specific schedule by ID. If omitted, returns all schedules.'), // }, // withUsageTracking('get-schedules', async ({ apiKey, scheduleId }) => { // try { // // Check backend version compatibility // await checkToolVersion('get-schedules'); // const actualApiKey = getApiKey(apiKey); // const url = scheduleId // ? `${API_BASE_URL}/api/schedules/${scheduleId}` // : `${API_BASE_URL}/api/schedules`; // const response = await fetch(url, { // method: 'GET', // headers: { // 'x-api-key': actualApiKey, // }, // }); // const result = await handleApiResponse(response); // const message = scheduleId // ? `Schedule details for ID: ${scheduleId}` // : 'All schedules'; // return await addBackgroundContext({ // content: [ // { // type: 'text', // text: formatSuccessMessage(message, result), // }, // ], // }); // } catch (error) { // const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; // return await addBackgroundContext({ // content: [ // { // type: 'text', // text: `Error retrieving schedules: ${errMsg}`, // }, // ], // isError: true, // }); // } // }) // ); // server.tool( // 'get-schedule-logs', // 'Get execution logs for a specific schedule with pagination', // { // apiKey: z // .string() // .optional() // .describe('API key for authentication (optional if provided via --api_key)'), // scheduleId: z.string().uuid().describe('The UUID of the schedule to get logs for'), // limit: z.number().int().positive().optional().default(50).describe('Number of logs to return (default: 50)'), // offset: z.number().int().nonnegative().optional().default(0).describe('Number of logs to skip (default: 0)'), // }, // withUsageTracking('get-schedule-logs', async ({ apiKey, scheduleId, limit, offset }) => { // try { // // Check backend version compatibility // await checkToolVersion('get-schedule-logs'); // const actualApiKey = getApiKey(apiKey); // const queryParams = new URLSearchParams(); // if (limit) queryParams.append('limit', limit.toString()); // if (offset) queryParams.append('offset', offset.toString()); // const response = await fetch( // `${API_BASE_URL}/api/schedules/${scheduleId}/logs?${queryParams}`, // { // method: 'GET', // headers: { // 'x-api-key': actualApiKey, // }, // } // ); // const result = await handleApiResponse(response); // return await addBackgroundContext({ // content: [ // { // type: 'text', // text: formatSuccessMessage(`Execution logs for schedule ${scheduleId}`, result), // }, // ], // }); // } catch (error) { // const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; // return await addBackgroundContext({ // content: [ // { // type: 'text', // text: `Error retrieving schedule logs: ${errMsg}`, // }, // ], // isError: true, // }); // } // }) // ); // server.tool( // 'delete-schedule', // 'Delete a cron job schedule permanently', // { // apiKey: z // .string() // .optional() // .describe('API key for authentication (optional if provided via --api_key)'), // scheduleId: z.string().uuid().describe('The UUID of the schedule to delete'), // }, // withUsageTracking('delete-schedule', async ({ apiKey, scheduleId }) => { // try { // // Check backend version compatibility // await checkToolVersion('delete-schedule'); // const actualApiKey = getApiKey(apiKey); // const response = await fetch(`${API_BASE_URL}/api/schedules/${scheduleId}`, { // method: 'DELETE', // headers: { // 'x-api-key': actualApiKey, // }, // }); // const result = await handleApiResponse(response); // return await addBackgroundContext({ // content: [ // { // type: 'text', // text: formatSuccessMessage(`Schedule ${scheduleId} deleted successfully`, result), // }, // ], // }); // } catch (error) { // const errMsg = error instanceof Error ? error.message : 'Unknown error occurred'; // return { // content: [ // { // type: 'text', // text: `Error deleting schedule: ${errMsg}`, // }, // ], // isError: true, // }; // } // }) // ); // Return the configured values for reference return { apiKey: GLOBAL_API_KEY, apiBaseUrl: API_BASE_URL, toolCount: 15, }; }

Implementation Reference

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/InsForge/insforge-mcp'

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