Skip to content

Commit 5c2334b

Browse files
hyperpolymathclaude
andcommitted
feat(cartridge): add burble-admin-mcp and idaptik-admin-mcp cartridges
Flywheel from game-admin-mcp pattern: burble-admin-mcp (10 MCP tools): check_health, list_rooms, create_room, close_room, kick_user, get_config, update_config, voice_stats, toggle_recording, node_status V adapter bridges Burble's Phoenix API (port 4000) idaptik-admin-mcp (10 MCP tools): server_status, list_sessions, create_session, end_session, get_config, update_config, list_level_packs, toggle_training, player_stats, server_action V adapter bridges IDApTIK's WebSocket management API (port 9101) Both include PanLL panel manifests (3 panels each). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent becc5a7 commit 5c2334b

4 files changed

Lines changed: 686 additions & 0 deletions

File tree

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
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+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"$schema": "https://panll.dev/schemas/panel-manifest/v1.json",
3+
"spdx": "PMPL-1.0-or-later",
4+
"cartridge": "burble-admin-mcp",
5+
"domain": "Voice Platform",
6+
"version": "0.1.0",
7+
"clade": "infrastructure/voice",
8+
"description": "MCP cartridge for Burble voice platform administration — monitor rooms, voice quality, and node health via BoJ",
9+
"panels": [
10+
{
11+
"id": "burble-server-status",
12+
"title": "Burble Server Status",
13+
"description": "Node health, uptime, active rooms count, and total participants",
14+
"type": "status-indicator",
15+
"data_source": {
16+
"endpoint": "/cartridge/burble-admin-mcp/invoke",
17+
"method": "POST",
18+
"body": { "tool": "node_status" },
19+
"refresh_interval_ms": 10000
20+
},
21+
"widgets": [
22+
{ "type": "state-badge", "field": "health", "label": "Health", "states": {
23+
"healthy": { "color": "#2ecc71", "icon": "check-circle" },
24+
"degraded": { "color": "#f39c12", "icon": "alert-circle" },
25+
"down": { "color": "#e74c3c", "icon": "x-circle" }
26+
}},
27+
{ "type": "text", "field": "uptime", "label": "Uptime" },
28+
{ "type": "counter", "field": "active_rooms", "label": "Active Rooms", "icon": "mic" },
29+
{ "type": "counter", "field": "total_participants", "label": "Participants", "icon": "users" }
30+
]
31+
},
32+
{
33+
"id": "burble-voice-quality",
34+
"title": "Voice Quality Metrics",
35+
"description": "WebRTC quality indicators — bandwidth, jitter, packet loss, and codec distribution",
36+
"type": "metric",
37+
"data_source": {
38+
"endpoint": "/cartridge/burble-admin-mcp/invoke",
39+
"method": "POST",
40+
"body": { "tool": "voice_stats" },
41+
"refresh_interval_ms": 5000
42+
},
43+
"widgets": [
44+
{ "type": "gauge", "field": "avg_bandwidth_kbps", "label": "Avg Bandwidth (kbps)", "min": 0, "max": 512 },
45+
{ "type": "gauge", "field": "avg_jitter_ms", "label": "Avg Jitter (ms)", "min": 0, "max": 100 },
46+
{ "type": "gauge", "field": "avg_packet_loss_pct", "label": "Avg Packet Loss (%)", "min": 0, "max": 10 },
47+
{ "type": "text", "field": "codec_distribution", "label": "Codec Distribution" }
48+
]
49+
},
50+
{
51+
"id": "burble-room-activity",
52+
"title": "Room Activity",
53+
"description": "Active rooms, peak participant count, and recordings in progress",
54+
"type": "metric",
55+
"data_source": {
56+
"endpoint": "/cartridge/burble-admin-mcp/invoke",
57+
"method": "POST",
58+
"body": { "tool": "list_rooms" },
59+
"refresh_interval_ms": 10000
60+
},
61+
"widgets": [
62+
{ "type": "counter", "field": "active_rooms", "label": "Active Rooms", "icon": "radio" },
63+
{ "type": "counter", "field": "peak_participants", "label": "Peak Participants", "icon": "trending-up" },
64+
{ "type": "counter", "field": "recordings_in_progress", "label": "Recordings", "icon": "disc" }
65+
]
66+
}
67+
]
68+
}

0 commit comments

Comments
 (0)