|
| 1 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | +// |
| 4 | +// Burble-Admin-MCP Cartridge — V-lang adapter layer. |
| 5 | +// |
| 6 | +// Bridges Burble's Elixir/Phoenix HTTP API to BoJ's MCP tools via the |
| 7 | +// BoJ triple adapter pattern. Provides MCP tools for monitoring, managing, |
| 8 | +// and configuring the Burble voice platform. All communication is over |
| 9 | +// HTTP to the Phoenix server — no FFI calls. |
| 10 | + |
| 11 | +module burble_admin_adapter |
| 12 | + |
| 13 | +import json |
| 14 | +import net.http |
| 15 | + |
| 16 | +// =================================================================== |
| 17 | +// MCP Tool definitions |
| 18 | +// =================================================================== |
| 19 | + |
| 20 | +// MCP tool metadata for BoJ cartridge discovery. |
| 21 | +pub const tools = [ |
| 22 | + ToolDef{ |
| 23 | + name: 'check_health' |
| 24 | + description: 'Check Burble server health status' |
| 25 | + input_schema: '{"type":"object","properties":{}}' |
| 26 | + }, |
| 27 | + ToolDef{ |
| 28 | + name: 'list_rooms' |
| 29 | + description: 'List all active voice rooms with participant counts' |
| 30 | + input_schema: '{"type":"object","properties":{}}' |
| 31 | + }, |
| 32 | + ToolDef{ |
| 33 | + name: 'create_room' |
| 34 | + description: 'Create a new voice room' |
| 35 | + input_schema: '{"type":"object","properties":{"name":{"type":"string"},"max_participants":{"type":"integer","default":16}},"required":["name"]}' |
| 36 | + }, |
| 37 | + ToolDef{ |
| 38 | + name: 'close_room' |
| 39 | + description: 'Close an active voice room and disconnect all participants' |
| 40 | + input_schema: '{"type":"object","properties":{"room_id":{"type":"string"}},"required":["room_id"]}' |
| 41 | + }, |
| 42 | + ToolDef{ |
| 43 | + name: 'kick_user' |
| 44 | + description: 'Remove a user from a voice room' |
| 45 | + input_schema: '{"type":"object","properties":{"room_id":{"type":"string"},"user_id":{"type":"string"}},"required":["room_id","user_id"]}' |
| 46 | + }, |
| 47 | + ToolDef{ |
| 48 | + name: 'get_config' |
| 49 | + description: 'Retrieve current Burble server configuration (TOML format)' |
| 50 | + input_schema: '{"type":"object","properties":{}}' |
| 51 | + }, |
| 52 | + ToolDef{ |
| 53 | + name: 'update_config' |
| 54 | + description: 'Update Burble server configuration' |
| 55 | + input_schema: '{"type":"object","properties":{"config_json":{"type":"string","description":"JSON-encoded config key-value pairs to update"}},"required":["config_json"]}' |
| 56 | + }, |
| 57 | + ToolDef{ |
| 58 | + name: 'voice_stats' |
| 59 | + description: 'Get WebRTC voice quality statistics (bandwidth, jitter, packet loss, codec)' |
| 60 | + input_schema: '{"type":"object","properties":{}}' |
| 61 | + }, |
| 62 | + ToolDef{ |
| 63 | + name: 'toggle_recording' |
| 64 | + description: 'Toggle recording on/off for a voice room' |
| 65 | + input_schema: '{"type":"object","properties":{"room_id":{"type":"string"}},"required":["room_id"]}' |
| 66 | + }, |
| 67 | + ToolDef{ |
| 68 | + name: 'node_status' |
| 69 | + description: 'Get BEAM node information (name, uptime, connections, memory)' |
| 70 | + input_schema: '{"type":"object","properties":{}}' |
| 71 | + }, |
| 72 | +] |
| 73 | + |
| 74 | +struct ToolDef { |
| 75 | + name string |
| 76 | + description string |
| 77 | + input_schema string |
| 78 | +} |
| 79 | + |
| 80 | +// =================================================================== |
| 81 | +// Adapter lifecycle |
| 82 | +// =================================================================== |
| 83 | + |
| 84 | +// BurbleAdminAdapter holds the connection state for communicating with |
| 85 | +// the Burble Phoenix API over HTTP. |
| 86 | +struct BurbleAdminAdapter { |
| 87 | +mut: |
| 88 | + base_url string |
| 89 | +} |
| 90 | + |
| 91 | +// Create a new adapter instance. |
| 92 | +// The default base URL targets Burble's Phoenix server on port 4000. |
| 93 | +pub fn new_adapter(base_url string) BurbleAdminAdapter { |
| 94 | + url := if base_url.len > 0 { base_url } else { 'http://[::1]:4000' } |
| 95 | + return BurbleAdminAdapter{ |
| 96 | + base_url: url |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +// =================================================================== |
| 101 | +// MCP tool dispatch |
| 102 | +// =================================================================== |
| 103 | + |
| 104 | +// Dispatch an MCP tool invocation. |
| 105 | +// Called by BoJ's cartridge router with the tool name and JSON arguments. |
| 106 | +pub fn (mut a BurbleAdminAdapter) invoke(tool_name string, args_json string) string { |
| 107 | + return match tool_name { |
| 108 | + 'check_health' { a.handle_check_health() } |
| 109 | + 'list_rooms' { a.handle_list_rooms() } |
| 110 | + 'create_room' { a.handle_create_room(args_json) } |
| 111 | + 'close_room' { a.handle_close_room(args_json) } |
| 112 | + 'kick_user' { a.handle_kick_user(args_json) } |
| 113 | + 'get_config' { a.handle_get_config() } |
| 114 | + 'update_config' { a.handle_update_config(args_json) } |
| 115 | + 'voice_stats' { a.handle_voice_stats() } |
| 116 | + 'toggle_recording' { a.handle_toggle_recording(args_json) } |
| 117 | + 'node_status' { a.handle_node_status() } |
| 118 | + else { '{"error":"unknown tool: ${tool_name}"}' } |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +// =================================================================== |
| 123 | +// HTTP helpers |
| 124 | +// =================================================================== |
| 125 | + |
| 126 | +// Perform a GET request against the Burble API and return the response body. |
| 127 | +fn (a &BurbleAdminAdapter) http_get(path string) string { |
| 128 | + resp := http.get('${a.base_url}${path}') or { |
| 129 | + return '{"error":"HTTP GET failed for ${path}: ${err.msg()}"}' |
| 130 | + } |
| 131 | + if resp.status_code < 200 || resp.status_code >= 300 { |
| 132 | + return '{"error":"HTTP ${resp.status_code} from ${path}: ${resp.body}"}' |
| 133 | + } |
| 134 | + return resp.body |
| 135 | +} |
| 136 | + |
| 137 | +// Perform a POST request with a JSON body against the Burble API. |
| 138 | +fn (a &BurbleAdminAdapter) http_post(path string, body string) string { |
| 139 | + resp := http.post_json('${a.base_url}${path}', body) or { |
| 140 | + return '{"error":"HTTP POST failed for ${path}: ${err.msg()}"}' |
| 141 | + } |
| 142 | + if resp.status_code < 200 || resp.status_code >= 300 { |
| 143 | + return '{"error":"HTTP ${resp.status_code} from ${path}: ${resp.body}"}' |
| 144 | + } |
| 145 | + return resp.body |
| 146 | +} |
| 147 | + |
| 148 | +// Perform a PUT request with a JSON body against the Burble API. |
| 149 | +fn (a &BurbleAdminAdapter) http_put(path string, body string) string { |
| 150 | + resp := http.fetch( |
| 151 | + url: '${a.base_url}${path}' |
| 152 | + method: .put |
| 153 | + header: http.new_header_from_map({ |
| 154 | + http.CommonHeader.content_type: 'application/json' |
| 155 | + }) |
| 156 | + data: body |
| 157 | + ) or { |
| 158 | + return '{"error":"HTTP PUT failed for ${path}: ${err.msg()}"}' |
| 159 | + } |
| 160 | + if resp.status_code < 200 || resp.status_code >= 300 { |
| 161 | + return '{"error":"HTTP ${resp.status_code} from ${path}: ${resp.body}"}' |
| 162 | + } |
| 163 | + return resp.body |
| 164 | +} |
| 165 | + |
| 166 | +// Perform a DELETE request against the Burble API. |
| 167 | +fn (a &BurbleAdminAdapter) http_delete(path string) string { |
| 168 | + resp := http.fetch( |
| 169 | + url: '${a.base_url}${path}' |
| 170 | + method: .delete |
| 171 | + ) or { |
| 172 | + return '{"error":"HTTP DELETE failed for ${path}: ${err.msg()}"}' |
| 173 | + } |
| 174 | + if resp.status_code < 200 || resp.status_code >= 300 { |
| 175 | + return '{"error":"HTTP ${resp.status_code} from ${path}: ${resp.body}"}' |
| 176 | + } |
| 177 | + return resp.body |
| 178 | +} |
| 179 | + |
| 180 | +// =================================================================== |
| 181 | +// Tool handlers |
| 182 | +// =================================================================== |
| 183 | + |
| 184 | +// check_health — GET /api/health |
| 185 | +// Returns the overall health status of the Burble server. |
| 186 | +fn (a &BurbleAdminAdapter) handle_check_health() string { |
| 187 | + return a.http_get('/api/health') |
| 188 | +} |
| 189 | + |
| 190 | +// list_rooms — GET /api/rooms |
| 191 | +// Returns all active voice rooms with participant counts. |
| 192 | +fn (a &BurbleAdminAdapter) handle_list_rooms() string { |
| 193 | + return a.http_get('/api/rooms') |
| 194 | +} |
| 195 | + |
| 196 | +// create_room — POST /api/rooms {name, max_participants} |
| 197 | +// Creates a new voice room on the Burble server. |
| 198 | +fn (a &BurbleAdminAdapter) handle_create_room(args_json string) string { |
| 199 | + parsed := json.decode(map[string]json.Any, args_json) or { |
| 200 | + return '{"error":"invalid JSON: ${err.msg()}"}' |
| 201 | + } |
| 202 | + name := (parsed['name'] or { return '{"error":"missing required field: name"}' }).str() |
| 203 | + max_participants := (parsed['max_participants'] or { json.Any(16) }).int() |
| 204 | + |
| 205 | + body := json.encode({ |
| 206 | + 'name': json.Any(name) |
| 207 | + 'max_participants': json.Any(max_participants) |
| 208 | + }) |
| 209 | + return a.http_post('/api/rooms', body) |
| 210 | +} |
| 211 | + |
| 212 | +// close_room — DELETE /api/rooms/{room_id} |
| 213 | +// Closes a voice room and disconnects all participants. |
| 214 | +fn (a &BurbleAdminAdapter) handle_close_room(args_json string) string { |
| 215 | + parsed := json.decode(map[string]json.Any, args_json) or { |
| 216 | + return '{"error":"invalid JSON: ${err.msg()}"}' |
| 217 | + } |
| 218 | + room_id := (parsed['room_id'] or { return '{"error":"missing required field: room_id"}' }).str() |
| 219 | + return a.http_delete('/api/rooms/${room_id}') |
| 220 | +} |
| 221 | + |
| 222 | +// kick_user — POST /api/rooms/{room_id}/kick {user_id} |
| 223 | +// Removes a specific user from a voice room. |
| 224 | +fn (a &BurbleAdminAdapter) handle_kick_user(args_json string) string { |
| 225 | + parsed := json.decode(map[string]json.Any, args_json) or { |
| 226 | + return '{"error":"invalid JSON: ${err.msg()}"}' |
| 227 | + } |
| 228 | + room_id := (parsed['room_id'] or { return '{"error":"missing required field: room_id"}' }).str() |
| 229 | + user_id := (parsed['user_id'] or { return '{"error":"missing required field: user_id"}' }).str() |
| 230 | + |
| 231 | + body := json.encode({ |
| 232 | + 'user_id': json.Any(user_id) |
| 233 | + }) |
| 234 | + return a.http_post('/api/rooms/${room_id}/kick', body) |
| 235 | +} |
| 236 | + |
| 237 | +// get_config — GET /api/config |
| 238 | +// Retrieves the current server configuration in TOML format. |
| 239 | +fn (a &BurbleAdminAdapter) handle_get_config() string { |
| 240 | + return a.http_get('/api/config') |
| 241 | +} |
| 242 | + |
| 243 | +// update_config — PUT /api/config {config_json} |
| 244 | +// Updates the Burble server configuration with the provided key-value pairs. |
| 245 | +fn (a &BurbleAdminAdapter) handle_update_config(args_json string) string { |
| 246 | + parsed := json.decode(map[string]json.Any, args_json) or { |
| 247 | + return '{"error":"invalid JSON: ${err.msg()}"}' |
| 248 | + } |
| 249 | + config_json := (parsed['config_json'] or { |
| 250 | + return '{"error":"missing required field: config_json"}' |
| 251 | + }).str() |
| 252 | + return a.http_put('/api/config', config_json) |
| 253 | +} |
| 254 | + |
| 255 | +// voice_stats — GET /api/stats |
| 256 | +// Returns WebRTC quality stats: bandwidth, jitter, packet loss, codec info. |
| 257 | +fn (a &BurbleAdminAdapter) handle_voice_stats() string { |
| 258 | + return a.http_get('/api/stats') |
| 259 | +} |
| 260 | + |
| 261 | +// toggle_recording — POST /api/rooms/{room_id}/recording |
| 262 | +// Toggles recording on or off for the specified voice room. |
| 263 | +fn (a &BurbleAdminAdapter) handle_toggle_recording(args_json string) string { |
| 264 | + parsed := json.decode(map[string]json.Any, args_json) or { |
| 265 | + return '{"error":"invalid JSON: ${err.msg()}"}' |
| 266 | + } |
| 267 | + room_id := (parsed['room_id'] or { return '{"error":"missing required field: room_id"}' }).str() |
| 268 | + return a.http_post('/api/rooms/${room_id}/recording', '{}') |
| 269 | +} |
| 270 | + |
| 271 | +// node_status — GET /api/node |
| 272 | +// Returns BEAM node info: name, uptime, connections, memory usage. |
| 273 | +fn (a &BurbleAdminAdapter) handle_node_status() string { |
| 274 | + return a.http_get('/api/node') |
| 275 | +} |
0 commit comments