Skip to main content
Glama

EdgeOne Pages MCP

Official
import COS from 'cos-nodejs-sdk-v5'; import * as path from 'path'; import * as fs from 'fs/promises'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); const BASE_API_URL1 = 'https://pages-api.cloud.tencent.com/v1'; const BASE_API_URL2 = 'https://pages-api.edgeone.ai/v1'; let BASE_API_URL = ''; // Console override for logging interface LogEntry { timestamp: string; level: string; message: string; } let deploymentLogs: LogEntry[] = []; let originalConsole: Console; const overrideConsole = () => { if (!originalConsole) { originalConsole = { ...console }; } const createLogFunction = (level: string, originalFn: Function) => { return (...args: any[]) => { const timestamp = new Date().toISOString(); const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg) ).join(' '); deploymentLogs.push({ timestamp, level, message }); // Call original console function originalFn.apply(console, args); }; }; console.log = createLogFunction('LOG', originalConsole.log); console.error = createLogFunction('ERROR', originalConsole.error); console.warn = createLogFunction('WARN', originalConsole.warn); console.info = createLogFunction('INFO', originalConsole.info); }; const restoreConsole = () => { if (originalConsole) { console.log = originalConsole.log; console.error = originalConsole.error; console.warn = originalConsole.warn; console.info = originalConsole.info; } }; const resetLogs = () => { deploymentLogs = []; }; const formatLogs = (): string => { if (deploymentLogs.length === 0) { return ''; } // Remove duplicates by keeping track of seen messages const seenMessages = new Set<string>(); const uniqueLogs = deploymentLogs.filter(log => { const key = `${log.level}: ${log.message}`; if (seenMessages.has(key)) { return false; } seenMessages.add(key); return true; }); const logLines = uniqueLogs.map(log => { return `${log.level}: ${log.message}`; }); return `Deployment Process Log:\n${'='.repeat(50)}\n${logLines.join('\n')}\n${'='.repeat(50)}\n\n`; }; // Export BASE_API_URL for use in other files export const getBaseApiUrl = () => BASE_API_URL; // Get API key from environment variable or use argument const getApiKey = () => { return process.env.EDGEONE_PAGES_API_TOKEN; }; // Get authorization header with API key export const getAuthorization = () => { const apiKey = getApiKey(); if (!apiKey) { throw new Error( 'Missing EDGEONE_PAGES_API_TOKEN. Please provide a token with --token or set it as an environment variable.' ); } return `Bearer ${apiKey}`; }; // Get projectName from environment variable const getProjectName = () => process.env.EDGEONE_PAGES_PROJECT_NAME || ''; let tempProjectName: string | undefined; const getTempProjectName = (): string => { if (!tempProjectName) { tempProjectName = `local-upload-${Date.now()}`; } return tempProjectName; }; const resetTempProjectName = (): void => { tempProjectName = undefined; }; // Types interface FileInfo { isDir: boolean; path: string; size: number; } interface CosFile { Bucket: string; Region: string; Key: string; FilePath: string; [key: `x-cos-meta-${string}`]: string; } interface CosTempTokenResponse { Code: number; Data: { Response: { RequestId: string; Bucket: string; Region: string; TargetPath: string; ExpiredTime: number; Expiration: string; Credentials: { Token: string; TmpSecretId: string; TmpSecretKey: string; }; }; }; Message: string; RequestId: string; } interface ApiResponse<T> { Code: number; Data: T; Message: string; RequestId: string; } interface Project { ProjectId: string; Name: string; Status: string; PresetDomain: string; CustomDomains?: Array<{ Status: string; Domain: string; }>; } interface ProjectsResponse { Response: { Projects: Project[]; }; } interface CreatePagesProjectResponse { Response: { ProjectId: string; }; } interface EncipherTokenResponse { Response: { Token: string; Timestamp: number; }; } interface DeploymentResult { DeploymentId: string; ProjectId: string; Status: string; Code: number; BuildCost: string; ViaMeta: string; Env: string; RepoBranch: string | null; RepoCommitHash: string; RepoCommitMsg: string | null; PreviewUrl: string; CreatedOn: string; ModifiedOn: string; CoverUrl: string | null; UsedInProd: boolean; MetaData: string; ProjectUrl: string; } interface UploadResult { success: boolean; targetPath?: string; error?: any; } // Token cache mechanism interface TokenCache { token: CosTempTokenResponse | null; cos: COS | null; } const tokenCache: TokenCache = { token: null, cos: null, }; // Reset token cache const resetTokenCache = () => { tokenCache.token = null; tokenCache.cos = null; }; // Utility functions const sleep = (ms: number): Promise<void> => { return new Promise((resolve) => setTimeout(resolve, ms)); }; const checkAndSetBaseUrl = async (): Promise<void> => { const res1 = await fetch(`${BASE_API_URL1}`, { method: 'POST', headers: { Authorization: getAuthorization(), 'Content-Type': 'application/json', }, body: JSON.stringify({ Action: 'DescribePagesProjects', PageNumber: 1, PageSize: 10, }), }); const res2 = await fetch(`${BASE_API_URL2}`, { method: 'POST', headers: { Authorization: getAuthorization(), 'Content-Type': 'application/json', }, body: JSON.stringify({ Action: 'DescribePagesProjects', PageNumber: 1, PageSize: 10, }), }); // Parse responses const json1 = (await res1 .json() .catch(() => ({ Code: -1 }))) as ApiResponse<ProjectsResponse>; const json2 = (await res2 .json() .catch(() => ({ Code: -1 }))) as ApiResponse<ProjectsResponse>; // Check if either endpoint worked if (json1.Code === 0) { BASE_API_URL = BASE_API_URL1; console.log('Using BASE_API_URL1 endpoint'); } else if (json2.Code === 0) { BASE_API_URL = BASE_API_URL2; console.log('Using BASE_API_URL2 endpoint'); } else { // Both endpoints failed throw new Error( 'Invalid EDGEONE_PAGES_API_TOKEN. Please check your API token. For more information, please refer to https://edgeone.ai/document/177158578324279296' ); } }; // API functions /** * Get temporary COS token for file uploads */ const getCosTempToken = async (): Promise<CosTempTokenResponse> => { // Return cached token if available if (tokenCache.token) { return tokenCache.token; } let body; if (getProjectName()) { const result = await describePagesProjects({ projectName: getProjectName(), }); if (result.Data.Response.Projects.length > 0) { body = { ProjectId: result.Data.Response.Projects[0].ProjectId }; } else { throw new Error(`Project ${getProjectName()} not found`); } } else { body = { ProjectName: getTempProjectName() }; } const res = await fetch(`${BASE_API_URL}`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: getAuthorization(), }, body: JSON.stringify( Object.assign(body, { Action: 'DescribePagesCosTempToken' }) ), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`API request failed (${res.status}): ${errorText}`); } const tokenResponse = (await res.json()) as CosTempTokenResponse; // Cache the token tokenCache.token = tokenResponse; return tokenResponse; }; /** * Get or create a project */ const getOrCreateProject = async (): Promise<ApiResponse<ProjectsResponse>> => { if (getProjectName()) { const result = await describePagesProjects({ projectName: getProjectName(), }); if (result.Data.Response.Projects.length > 0) { console.log( `[getOrCreateProject] Project ${getProjectName()} already exists. Using existing project.` ); return result; } } console.log( `[getOrCreateProject] ProjectName is not provided. Creating new project.` ); return await createPagesProject(); }; /** * Create a new pages project */ const createPagesProject = async (): Promise<ApiResponse<ProjectsResponse>> => { try { const res = await fetch(`${BASE_API_URL}`, { method: 'POST', headers: { Authorization: getAuthorization(), 'Content-Type': 'application/json', }, body: JSON.stringify({ Action: 'CreatePagesProject', Name: getProjectName() || getTempProjectName(), Provider: 'Upload', Channel: 'Custom', Area: 'global', }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`API request failed (${res.status}): ${errorText}`); } const data = (await res.json()) as ApiResponse<CreatePagesProjectResponse>; const projectInfo = await describePagesProjects({ projectId: data?.Data?.Response?.ProjectId, }); return projectInfo; } catch (error) { console.error('Error creating pages project: ' + error); throw error; } }; /** * Describe pages projects */ const describePagesProjects = async (opts: { projectId?: string; projectName?: string; }): Promise<ApiResponse<ProjectsResponse>> => { const { projectId, projectName } = opts; const filters = []; if (projectId) { filters.push({ Name: 'ProjectId', Values: [projectId] }); } if (projectName) { filters.push({ Name: 'Name', Values: [projectName] }); } const res = await fetch(`${BASE_API_URL}`, { method: 'POST', headers: { Authorization: getAuthorization(), 'Content-Type': 'application/json', }, body: JSON.stringify({ Action: 'DescribePagesProjects', Filters: filters, Offset: 0, Limit: 10, OrderBy: 'CreatedOn', }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`API request failed (${res.status}): ${errorText}`); } return (await res.json()) as ApiResponse<ProjectsResponse>; }; /** * Describe pages deployments */ const describePagesDeployments = async ( projectId: string ): Promise<ApiResponse<any>> => { const res = await fetch(`${BASE_API_URL}`, { method: 'POST', headers: { Authorization: getAuthorization(), 'Content-Type': 'application/json', }, body: JSON.stringify({ Action: 'DescribePagesDeployments', ProjectId: projectId, Offset: 0, Limit: 50, OrderBy: 'CreatedOn', Order: 'Desc', }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`API request failed (${res.status}): ${errorText}`); } return (await res.json()) as ApiResponse<any>; }; /** * Describe pages encipher token */ const describePagesEncipherToken = async ( url: string ): Promise<ApiResponse<EncipherTokenResponse>> => { const res = await fetch(`${BASE_API_URL}`, { method: 'POST', headers: { Authorization: getAuthorization(), 'Content-Type': 'application/json', }, body: JSON.stringify({ Action: 'DescribePagesEncipherToken', Text: url, }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`API request failed (${res.status}): ${errorText}`); } return (await res.json()) as ApiResponse<EncipherTokenResponse>; }; /** * Check if a path is a zip file */ const isZipFile = (filePath: string): boolean => { return path.extname(filePath).toLowerCase() === '.zip'; }; /** * Create a new deployment */ const createPagesDeployment = async (opts: { projectId: string; targetPath: string; isZip: boolean; env: 'Production' | 'Preview'; }): Promise<ApiResponse<any>> => { const { projectId, targetPath, isZip, env } = opts; const res = await fetch(`${BASE_API_URL}`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: getAuthorization(), }, body: JSON.stringify({ Action: 'CreatePagesDeployment', ProjectId: projectId, ViaMeta: 'Upload', Provider: 'Upload', Env: env, DistType: isZip ? 'Zip' : 'Folder', TempBucketPath: targetPath, }), }); if (!res.ok) { const errorText = await res.text(); throw new Error( `[createPagesDeployment] API request failed (${res.status}): ${errorText}` ); } const data = (await res.json()) as ApiResponse<any>; if (data?.Data?.Response?.Error) { throw new Error( `[createPagesDeployment] Deployment creation failed: ${data.Data.Response.Error.Message}` ); } return data; }; // COS (Cloud Object Storage) functions // Initialize COS with dynamic authentication const getCosInstance = async (): Promise<COS> => { if (tokenCache.cos) { return tokenCache.cos; } const result = await getCosTempToken(); if ( result.Code !== 0 || !result.Data || !result.Data.Response || !result.Data.Response.Credentials ) { throw new Error('Failed to get COS temp token'); } const response = result.Data.Response; const credentials = response.Credentials; const cos = new COS({ SecretId: credentials.TmpSecretId, SecretKey: credentials.TmpSecretKey, SecurityToken: credentials.Token, }); tokenCache.cos = cos; return cos; }; /** * Recursively list all files in a directory */ const fastListFolder = async (rootPath: string): Promise<FileInfo[]> => { const list: FileInfo[] = []; const deep = async (dirPath: string): Promise<void> => { const files = await fs.readdir(dirPath, { withFileTypes: true }); for (const file of files) { const filePath = path.join(dirPath, file.name); const isDir = file.isDirectory(); const stats = await fs.stat(filePath); list.push({ isDir, path: isDir ? `${filePath}/` : filePath, size: isDir ? 0 : stats.size, }); if (isDir) { await deep(filePath); } } }; try { await deep(rootPath); if (list.length > 1000000) { throw new Error('too_much_files'); } return list; } catch (error) { console.error('Error in fastListFolder: ' + error); throw error; } }; /** * Convert file list to COS format */ const getFiles = ( list: FileInfo[], localFolder: string, bucket: string, region: string, targetPath: string ): CosFile[] => { return list .map((file) => { const filename = path .relative(localFolder, file.path) .replace(/\\/g, '/'); const Key = `${targetPath}/${filename || ''}`; return { Bucket: bucket, Region: region, Key, FilePath: file.path, }; }) .filter((file) => { if (file.FilePath.endsWith('/')) { return false; } if (file.Key === '/' || file.Key === '') { return false; } return true; }); }; /** * Upload files to COS */ const uploadFiles = async (files: CosFile[]): Promise<any> => { const cos = await getCosInstance(); return new Promise((resolve, reject) => { cos.uploadFiles( { files: files, SliceSize: 1024 * 1024, }, function (err, data) { if (err) { reject(err); } else { resolve(data); } } ); }); }; /** * Upload a directory or zip file to EdgeOne COS */ const uploadToEdgeOneCOS = async (localPath: string): Promise<UploadResult> => { try { const tokenResult = await getCosTempToken(); if (tokenResult.Code !== 0 || !tokenResult?.Data?.Response) { throw new Error( `Failed to get COS token: ${ tokenResult.Message || 'Invalid token response' }` ); } const response = tokenResult.Data.Response; const bucket = response.Bucket; const region = response.Region; const targetPath = response.TargetPath; if (!bucket || !region || !targetPath) { throw new Error( 'Missing required COS parameters (Bucket, Region, TargetPath) in token response.' ); } const isZip = isZipFile(localPath); if (isZip) { // Upload single zip file to COS console.log( `[uploadToEdgeOneCOS] Uploading zip file to COS with targetPath: ${targetPath}...` ); const fileName = path.basename(localPath); const key = `${targetPath}/${fileName}`; // Read the file data const fileBuffer = await fs.readFile(localPath); const fileStats = await fs.stat(localPath); const cos = await getCosInstance(); return new Promise((resolve, reject) => { cos.putObject( { Bucket: bucket, Region: region, Key: key, Body: fileBuffer, ContentLength: fileStats.size, }, function (err) { if (err) { console.error('Error uploading zip file to COS: ' + err); reject(err); } else { console.log(`[uploadToEdgeOneCOS] Upload successful.`); resolve({ success: true, targetPath: key, }); } } ); }); } else { // List all files in the directory const folderList = await fastListFolder(localPath); // Convert to COS format const files = getFiles(folderList, localPath, bucket, region, targetPath); // Upload files to COS console.log( `[uploadToEdgeOneCOS] Uploading ${files.length} files to COS with targetPath: ${targetPath}...` ); await uploadFiles(files); console.log(`[uploadToEdgeOneCOS] Upload successful.`); return { success: true, targetPath, }; } } catch (error) { console.error('Error uploading to COS: ' + error); throw error; } }; /** * Poll for deployment status until it's no longer processing */ const pollProjectStatus = async ( projectId: string, deploymentId: string ): Promise<DeploymentResult> => { let isProcessing = true; let deployment = null; while (isProcessing) { // Get list of deployments const deploymentsResult = await describePagesDeployments(projectId); // Find the specific deployment by deploymentId deployment = deploymentsResult.Data.Response.Deployments.find( (deploy: any) => deploy.DeploymentId === deploymentId ); if (!deployment) { throw new Error(`Deployment with ID ${deploymentId} not found`); } console.log(`[pollProjectStatus] Deployment status: ${deployment.Status}`); // Check if deployment is still processing if (deployment.Status !== 'Process') { isProcessing = false; } else { // Wait before next poll await sleep(5000); } } return deployment as DeploymentResult; }; /** * Validate that the path exists and is a directory or zip file */ const validateFolder = async (localPath: string): Promise<boolean> => { try { await fs.access(localPath); } catch (error) { throw new Error('localPath does not exist'); } const stats = await fs.stat(localPath); const isZip = isZipFile(localPath); if (!stats.isDirectory() && !isZip) { throw new Error('localPath must be a folder or zip file'); } return isZip; }; /** * Get structured deployment result * @param deploymentResult The result from polling deployment status * @param projectId The project ID * @param env Environment to deploy to, either 'Production' or 'Preview' * @returns Structured deployment result with type and url */ const getDeploymentStructuredResult = async ( deploymentResult: DeploymentResult, projectId: string, env: 'Production' | 'Preview' = 'Production' ): Promise<{ type: string; url: string; projectId: string }> => { // Get project details to get domain information const projectStatusResult = await describePagesProjects({ projectId: projectId, }); if (!projectStatusResult?.Data?.Response?.Projects?.[0]) { throw new Error('Failed to retrieve project status information.'); } const project = projectStatusResult.Data.Response.Projects[0]; // Check deployment status if (deploymentResult.Status === 'Success') { // For Production environment, check for custom domains if ( env === 'Production' && project.CustomDomains && project.CustomDomains.length > 0 ) { const customDomain = project.CustomDomains[0]; if (customDomain.Status === 'Pass') { return { type: 'custom', url: `https://${customDomain.Domain}`, projectId, }; } } // Process domain information const domain = deploymentResult.PreviewUrl ? deploymentResult.PreviewUrl.replace('https://', '') : project.PresetDomain; const encipherTokenResult = await describePagesEncipherToken(domain); if ( encipherTokenResult.Code !== 0 || !encipherTokenResult?.Data?.Response?.Token || !encipherTokenResult?.Data?.Response?.Timestamp ) { throw new Error( `Deployment completed, but failed to get access token: ${ encipherTokenResult.Message || 'Invalid token data' }` ); } const { Token, Timestamp } = encipherTokenResult.Data.Response; const url = `https://${domain}?eo_token=${Token}&eo_time=${Timestamp}`; return { type: 'temporary', url: url, projectId, }; } else { console.error( `[getDeploymentStructuredResult] Deployment failed with status: ${deploymentResult.Status}` ); throw new Error( `Deployment failed with status: ${deploymentResult.Status}` ); } }; /** * Deploy a local folder or zip file to EdgeOne Pages * @param localPath Path to the local folder or zip file to deploy * @param env Environment to deploy to, either 'Production' or 'Preview' * @returns URL to the deployed site */ export const deployFolderOrZipToEdgeOne = async ( localPath: string, env: 'Production' | 'Preview' = 'Production' ): Promise<string> => { // Reset logs and override console at the start resetLogs(); overrideConsole(); try { // Reset token cache at the start of deployment resetTokenCache(); resetTempProjectName(); // Validate folder or zip file const isZip = await validateFolder(localPath); await checkAndSetBaseUrl(); // 1. Upload folder to COS const uploadResult = await uploadToEdgeOneCOS(localPath); if (!uploadResult.targetPath) { throw new Error('COS upload succeeded but targetPath is missing.'); } const targetPath = uploadResult.targetPath; // 2. Get or create project console.log(`[getOrCreateProject] Getting or creating project...`); const projectResult = await getOrCreateProject(); if (!projectResult?.Data?.Response?.Projects?.[0]?.ProjectId) { console.error('Invalid project data received: ' + projectResult); throw new Error('Failed to retrieve Project ID after get/create.'); } const projectId = projectResult.Data.Response.Projects[0].ProjectId; console.log(`[getOrCreateProject] Using Project ID: ${projectId}`); // 3. Create deployment console.log( `[createPagesDeployment] Creating deployment in ${env} environment...` ); const res = await createPagesDeployment({ projectId, targetPath: targetPath, isZip, env, }); const deploymentId = res.Data.Response.DeploymentId; // 4. Wait for deployment to complete console.log( `[pollProjectStatus] Waiting for deployment to complete (polling status)...` ); await sleep(5000); const deploymentResult = await pollProjectStatus(projectId, deploymentId); // 5. Get structured deployment result and format message const structuredResult = await getDeploymentStructuredResult( deploymentResult, projectId, env ); /** * Format deployment result into user-friendly message * @param deploymentResult The structured deployment result * @returns Text message describing the deployment status */ const formatDeploymentMessage = async (deploymentResult: { type: string; url: string; }): Promise<string> => { const res = await fetch('https://proxy.edgeone.site/mcp-format', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(deploymentResult), }); const { text } = await res.json(); return text; }; const text = await formatDeploymentMessage(structuredResult); // Append deployment logs to the result const logs = formatLogs(); const finalText = `${logs}${text}`; return finalText; } catch (error) { // Ensure logs are captured even on error const logs = formatLogs(); const errorMessage = error instanceof Error ? error.message : String(error); const finalText = `${logs}Deployment failed: ${errorMessage}`; throw new Error(finalText); } finally { // Always restore console restoreConsole(); } };

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/TencentEdgeOne/edgeone-pages-mcp'

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