awaBerry MCP Server Documentation -> Cloudflare worker implementation of the MCP Server

Cloudflare worker implementation of the MCP Server

Summary

The provided code implements an MCP (Model Context Protocol) Server as a Cloudflare Worker. It facilitates secure remote device management through awaBerry Remote, allowing connections to devices, execution of terminal commands, and file transfers without direct SSH/SCP or firewall configurations. The server exposes two main tools: connect_to_device for establishing a persistent terminal session, and execute_terminal_command for running shell commands on the connected device. It handles API interactions with agentic.awaberry.net for project and device management, session initiation, and command execution, incorporating connection status polling and robust error handling. The Cloudflare Worker acts as an HTTP endpoint /mcp, processing incoming MCP requests and relaying responses via a custom transport layer.

Description

Overview

This Cloudflare Worker provides a server-side implementation of the Model Context Protocol (MCP) using the @modelcontextprotocol/sdk/server/mcp.js library. It integrates with the awaBerry Remote API to offer secure, firewall-agnostic device connectivity and remote command execution. The worker exposes an HTTP endpoint at /mcp to handle incoming MCP requests.

Internal API Functions

The worker includes several utility functions for interacting with the awaBerry API:

  • AWABERRY_API_URL: Constant defining the base URL for awaBerry API requests.
  • callApi(endpoint, body): An asynchronous helper to make POST requests to the awaBerry API, handling JSON serialization and error checking.
  • sleep(ms): A utility function to pause execution for a given number of milliseconds.
  • connectToDevice(projectKey, projectSecret, deviceName): Establishes a connection to a specified device. It first retrieves project data, validates the device name against available devices, initiates a session, and then polls for the device connection status until established or a timeout occurs.
  • executeCommand(sessionToken, deviceuuid, command): Sends a shell command to an already connected device using the provided session token and device UUID.

Code: Internal API Functions


import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';

// --- Internal API Functions ---

const AWABERRY_API_URL = "https://agentic.awaberry.net/apirequests";

async function callApi(endpoint, body) {
    const res = await fetch(AWABERRY_API_URL + endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
    });
    if (!res.ok) {
        const errorText = await res.text();
        throw new Error(`awaBerry API Error: ${errorText}`);
    }
    return res.json();
}

function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}

async function connectToDevice(projectKey, projectSecret, deviceName) {
    const projectData = await callApi('/getProject', { projectkey: projectKey, projectsecret: projectSecret });
    if (!projectData || !projectData.agentprojectsetup) {
        throw new Error('Incorrect projectKey / projectSecret');
    }

    const agentSetup = JSON.parse(projectData.agentprojectsetup);
    const devices = agentSetup.setupEntries || [];
    const targetDevice = devices.find((entry) => entry.deviceName === deviceName);

    if (!targetDevice) {
        const available = devices.map(d => d.deviceName).join(', ');
        throw new Error(`Device '${deviceName}' not found. Available devices: ${available}`);
    }

    const { deviceuuid } = targetDevice;
    const { sessionToken } = await callApi('/initSession', { projectkey: projectKey, projectsecret: projectSecret });

    let isConnected = await callApi('/startDeviceConnection', { sessionToken, deviceuuid });

    if (!isConnected) {
        for (let i = 0; i < 20; i++) {
            await sleep(2000);
            isConnected = await callApi('/getDeviceConnectionStatus', { sessionToken, deviceuuid });
            if (isConnected) break;
        }
    }

    return {
        sessionToken,
        status: isConnected ? 'connected' : 'notconnected',
        deviceuuid,
    };
}

async function executeCommand(sessionToken, deviceuuid, command) {
    return callApi('/executeCommand', { sessionToken, deviceuuid, command });
}
    

MCP Server Implementation

The createMcpServer(env) function initializes an McpServer instance and registers two tools:

  • connect_to_device:
    • Description: Connects to a remote device securely via awaBerry Remote.
    • Arguments: projectKey (optional, defaults to env.AWABERRY_PROJECT_KEY), projectSecret (optional, defaults to env.AWABERRY_PROJECT_SECRET), and deviceName (required).
    • Functionality: Uses the internal connectToDevice function to establish a connection and returns the session token, connection status, and device UUID.
  • execute_terminal_command:
    • Description: Executes a shell command on the connected device.
    • Arguments: sessionToken, deviceuuid, and command (all required).
    • Functionality: Uses the internal executeCommand function and returns the API response, formatted for MCP.

Code: MCP Server Tools


// --- MCP Server Implementation ---

function createMcpServer(env) {
    const server = new McpServer({
        name: 'awaberry-mcp-server-worker',
        version: '1.0.0',
    });

    server.tool(
        'connect_to_device',
        'Connect to a device via awaBerry Remote without the need for ssh/scp/firewalls. This establishes a secure terminal connection to manage files, connect to databases and execute terminal commands in a persistent long running terminal session.',
        {
            projectKey: z.string().optional().describe('Authentication key AWABERRY_PROJECT_KEY'),
            projectSecret: z.string().optional().describe('Authentication secret AWABERRY_PROJECT_SECRET'),
            deviceName: z.string().describe('The name of the device to connect to'),
        },
        async (args) => {
            const key = args.projectKey || env.AWABERRY_PROJECT_KEY;
            const secret = args.projectSecret || env.AWABERRY_PROJECT_SECRET;

            if (!key || !secret) {
                throw new Error('Project key and secret are required.');
            }

            const result = await connectToDevice(key, secret, args.deviceName);
            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify(result, null, 2)
                    }
                ]
            };
        }
    );

    server.tool(
        'execute_terminal_command',
        'Executes a shell command on the connected device',
        {
            sessionToken: z.string().describe('Session token from connect_to_device'),
            deviceuuid: z.string().describe('Device UUID from connect_to_device'),
            command: z.string().describe('Shell command to execute'),
        },
        async (args) => {
            const apiResponse = await executeCommand(args.sessionToken, args.deviceuuid, args.command);

            // Extract the actual result from the API response
            const result = apiResponse.result || apiResponse;

            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify({
                            success: apiResponse.success !== false,
                            result: result
                        })
                    }
                ],
                // Include structured content for better integration
                structuredContent: {
                    success: apiResponse.success !== false,
                    result: result
                }
            };
        }
    );

    return server;
}
    

Cloudflare Worker fetch Handler

The default export is an object with an asynchronous fetch method, which is the entry point for Cloudflare Workers:

  • CORS Handling: Responds to OPTIONS requests with appropriate CORS headers.
  • Routing: Only handles POST requests to the /mcp path; all other requests receive a 404.
  • MCP Request Processing:
    • Initializes the MCP server using createMcpServer(env).
    • Parses the incoming request body as JSON.
    • Sets up a custom transport layer to collect response messages from the MCP server.
    • Connects the server to the custom transport.
    • Invokes the server's message handler (transport.onmessage) with the request body.
    • Waits for the first response message (or a 30-second timeout).
    • Returns the collected MCP response(s) as JSON, including CORS headers.
  • Error Handling: Catches any errors during processing and returns a 500 status with an error message.

Code: Cloudflare Worker Entry Point


export default {
    async fetch(request, env, ctx) {
        const url = new URL(request.url);

        // Handle OPTIONS for CORS preflight
        if (request.method === 'OPTIONS') {
            return new Response(null, {
                headers: {
                    'Access-Control-Allow-Origin': '*',
                    'Access-Control-Allow-Methods': 'POST, OPTIONS',
                    'Access-Control-Allow-Headers': 'Content-Type',
                },
            });
        }

        if (url.pathname !== '/mcp' || request.method !== 'POST') {
            return new Response('Not Found', { status: 404 });
        }

        try {
            const server = createMcpServer(env);
            const requestBody = await request.json();

            // Collect all response messages
            const responseMessages = [];
            let resolveResponse;
            const responsePromise = new Promise(resolve => {
                resolveResponse = resolve;
            });

            let responseCount = 0;

            // Custom transport that collects messages
            const transport = {
                start: async () => {},
                send: async (message) => {
                    responseMessages.push(message);
                    responseCount++;
                    // Resolve after first response (for request/response pattern)
                    if (responseCount === 1) {
                        resolveResponse();
                    }
                },
                close: async () => {
                    resolveResponse();
                },
                onmessage: undefined
            };

            // Connect server to transport
            await server.connect(transport);

            // Store the onmessage callback that was set by the server
            const messageCallback = transport.onmessage;

            // Invoke the callback with the request
            if (messageCallback) {
                // Call the callback and handle both sync and async cases
                try {
                    const result = messageCallback(requestBody);
                    if (result && typeof result.catch === 'function') {
                        result.catch(error => {
                            console.error('Error in message callback:', error);
                            resolveResponse();
                        });
                    }
                } catch (error) {
                    console.error('Error invoking message callback:', error);
                    resolveResponse();
                }
            } else {
                throw new Error('MCP server did not set up message handler');
            }

            // Wait for response with timeout
            const timeoutPromise = new Promise(resolve =>
                setTimeout(() => resolve(), 30000) // 30 second timeout
            );

            await Promise.race([responsePromise, timeoutPromise]);

            // Return collected messages
            const response = responseMessages.length === 1
                ? responseMessages[0]
                : responseMessages;

            return new Response(JSON.stringify(response), {
                headers: {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*',
                    'Access-Control-Allow-Methods': 'POST, OPTIONS',
                    'Access-Control-Allow-Headers': 'Content-Type',
                },
            });
        } catch (error) {
            console.error('Error processing MCP request:', error);
            return new Response(JSON.stringify({
                error: {
                    code: -32603,
                    message: error.message
                }
            }), {
                status: 500,
                headers: {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*',
                },
            });
        }
    },
};