User Manual

awaBerry | Cloudflare Worker-Implementierung des MCP-Servers

Stellen Sie den awaBerry MCP-Server auf Cloudflare Workers bereit. Schritt-für-Schritt-Anleitung zum Bereitstellen der MCP-Oberfläche über das globale...

Cloudflare Worker Implementierung des MCP Servers

Zusammenfassung

Der bereitgestellte Code implementiert einen MCP (Model Context Protocol) Server als Cloudflare Worker. Er ermöglicht die sichere Fernverwaltung von Geräten über awaBerry Remote, erlaubt Verbindungen zu Geräten, die Ausführung von Terminalbefehlen und Dateiübertragungen ohne direkte SSH/SCP- oder Firewall-Konfigurationen. Der Server stellt zwei Hauptwerkzeuge bereit: connect_to_device zur Herstellung einer persistenten Terminalsitzung und execute_terminal_command zur Ausführung von Shell-Befehlen auf dem verbundenen Gerät. Er verarbeitet API-Interaktionen mit agentic.awaberry.net für Projekt- und Gerätemanagement, Sitzungsinitiierung und Befehlsausführung, einschließlich Abfrage des Verbindungsstatus und robuster Fehlerbehandlung. Der Cloudflare Worker fungiert als HTTP-Endpunkt /mcp, verarbeitet eingehende MCP-Anfragen und leitet Antworten über eine benutzerdefinierte Transportschicht weiter.

Beschreibung

Übersicht

Dieser Cloudflare Worker bietet eine serverseitige Implementierung des Model Context Protocol (MCP) unter Verwendung der Bibliothek @modelcontextprotocol/sdk/server/mcp.js. Er integriert sich in die awaBerry Remote API, um eine sichere, Firewall-unabhängige Geräteverbindung und Remote-Befehlsausführung zu ermöglichen. Der Worker stellt einen HTTP-Endpunkt unter /mcp bereit, um eingehende MCP-Anfragen zu bearbeiten.

Interne API-Funktionen

Der Worker enthält mehrere Hilfsfunktionen für die Interaktion mit der awaBerry API:

  • AWABERRY_API_URL: Konstante, die die Basis-URL für awaBerry API-Anfragen definiert.
  • callApi(endpoint, body): Eine asynchrone Hilfsfunktion zum Senden von POST-Anfragen an die awaBerry API, die JSON-Serialisierung und Fehlerprüfung handhabt.
  • sleep(ms): Eine Hilfsfunktion zum Pausieren der Ausführung für eine angegebene Anzahl von Millisekunden.
  • connectToDevice(projectKey, projectSecret, deviceName): Stellt eine Verbindung zu einem bestimmten Gerät her. Zuerst werden Projektdaten abgerufen, der Gerätename mit den verfügbaren Geräten validiert, eine Sitzung initiiert und dann der Gerätestatus abgefragt, bis die Verbindung hergestellt ist oder ein Timeout auftritt.
  • executeCommand(sessionToken, deviceuuid, command): Sendet einen Shell-Befehl an ein bereits verbundenes Gerät unter Verwendung des bereitgestellten Sitzungstokens und der Geräte-UUID.

Code: Interne API-Funktionen


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

// --- Interne API-Funktionen ---

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 Implementierung

Die Funktion createMcpServer(env) initialisiert eine McpServer-Instanz und registriert zwei Werkzeuge:

  • connect_to_device:
    • Beschreibung: Stellt eine sichere Verbindung zu einem entfernten Gerät über awaBerry Remote her.
    • Argumente: projectKey (optional, Standardwert ist env.AWABERRY_PROJECT_KEY), projectSecret (optional, Standardwert ist env.AWABERRY_PROJECT_SECRET) und deviceName (erforderlich).
    • Funktionalität: Verwendet die interne Funktion connectToDevice, um eine Verbindung herzustellen, und gibt das Sitzungstoken, den Verbindungsstatus und die Geräte-UUID zurück.
  • execute_terminal_command:
    • Beschreibung: Führt einen Shell-Befehl auf dem verbundenen Gerät aus.
    • Argumente: sessionToken, deviceuuid und command (alle erforderlich).
    • Funktionalität: Verwendet die interne Funktion executeCommand und gibt die API-Antwort formatiert für MCP zurück.

Code: MCP Server Werkzeuge


// --- MCP Server Implementierung ---

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

Der Standardexport ist ein Objekt mit einer asynchronen fetch-Methode, die der Einstiegspunkt für Cloudflare Workers ist:

  • CORS-Handling: Beantwortet OPTIONS-Anfragen mit entsprechenden CORS-Headern.
  • Routing: Verarbeitet nur POST-Anfragen an den Pfad /mcp; alle anderen Anfragen erhalten eine 404.
  • MCP-Anfrageverarbeitung:
    • Initialisiert den MCP-Server mit createMcpServer(env).
    • Parst den eingehenden Anfragetext als JSON.
    • Richtet eine benutzerdefinierte Transportschicht ein, um Antwortnachrichten vom MCP-Server zu sammeln.
    • Verbindet den Server mit dem benutzerdefinierten Transport.
    • Ruft den Nachrichtenhandler des Servers (transport.onmessage) mit dem Anfragetext auf.
    • Wartet auf die erste Antwortnachricht (oder ein 30-Sekunden-Timeout).
    • Gibt die gesammelten MCP-Antworten als JSON zurück, einschließlich CORS-Headern.
  • Fehlerbehandlung: Fängt alle Fehler während der Verarbeitung ab und gibt einen 500-Status mit einer Fehlermeldung zurück.

Code: Cloudflare Worker Einstiegspunkt


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': '*',
                },
            });
        }
    },
};