Extension MPB (MyDisct Pulse Bridge)
MPB is the live telemetry bridge used by the MyDisct Solver browser extension. It reports what the extension is solving in real time, exposes normalized task status endpoints, and lets your automation scripts track processing and completion states with stable task IDs.
MPB is extension-side runtime visibility. It is independent from createTask/fetchResult API flow and mirrors the extension's own live state. If extension starts solving any supported captcha type, MPB reports that task as processing. When extension finishes, MPB reports completed or failed.
Some examples in this page use HCAPTCHA for readability. The same MPB workflow and status model apply to all captcha types supported by the extension.
Core Concept
The extension sends periodic snapshots to your local bridge server, and the bridge converts these snapshots into queryable task records. Your Puppeteer or automation process only needs to read MPB endpoints to understand what the extension is doing now.
| Layer | Responsibility | Output |
|---|---|---|
| Browser Extension | Detects captcha, starts solve flow, emits progress and result events | Live snapshots with active tasks and events |
| MPB Bridge Server | Receives snapshots, builds in-memory task index, normalizes fields | Task list and task status endpoints |
| Your Automation App | Polls task IDs and status transitions | Reliable processing/completed/failed tracking |
Setup and Activation
Step 1: Start Bridge Server
Run the MPB bridge locally. Default address is http://127.0.0.1:3000.
node mpb-bridge-server.js
Step 2: Configure Extension MPB URL
Open extension popup settings and enable MPB. Set localhost bridge URL to your running server,
for example http://127.0.0.1:3000.
Step 3: Verify Connectivity
Use the base endpoint to confirm the bridge is reachable.
curl http://127.0.0.1:3000/
Expected Response
{
"ok": true,
"service": "MyDisct Pulse Bridge",
"endpoint": "/mpb/v1/active-captchas",
"tasks_endpoint": "/mpb/v1/tasks",
"task_status_endpoint": "/mpb/v1/tasks/:taskId"
}
Snapshot Endpoint
This endpoint is called by the extension. It sends current active captcha list and event stream. You normally do not post manually in production, but it is useful for testing.
POST Body Fields
| Field | Type | Required | Description |
|---|---|---|---|
source |
string | optional | Snapshot source name, usually mydisct_solver |
mode |
string | optional | Operating mode, usually extension_live |
ts |
number | optional | Unix milliseconds timestamp |
active_captchas |
array | required | List of currently processing extension tasks |
events |
array | required | Task event list including status transitions |
Example Snapshot Payload
{
"source": "mydisct_solver",
"mode": "extension_live",
"ts": 1760645076572,
"active_captchas": [
{
"extension_task_id": "mpb_mlpm1zv1_svwo4j",
"captcha_type": "HCAPTCHA",
"status": "processing",
"tab_id": 7,
"frame_id": 0,
"page_url": "https://target-site.example/login"
}
],
"events": [
{
"extension_task_id": "mpb_mlpm1zv1_svwo4j",
"captcha_type": "HCAPTCHA",
"status": "processing",
"source": "mydisct_solver",
"ts": 1760645076572
}
]
}
Acknowledgement Response
{
"ok": true,
"total_active": 1,
"total_events": 3,
"total_tasks": 8
}
Returns the latest raw snapshot exactly as received by the bridge.
Task Endpoints
Returns normalized task records built from active snapshots and events. This is the recommended endpoint for automation dashboards.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
status |
string | optional | Filter by task status: processing, completed, failed |
captcha_type |
string | optional | Filter by extension captcha type such as HCAPTCHA or RECAPTCHA |
Example Request
curl "http://127.0.0.1:3000/mpb/v1/tasks?status=processing&captcha_type=HCAPTCHA"
Example Response
{
"ok": true,
"total": 1,
"tasks": [
{
"task_id": "mpb_mlpm1zv1_svwo4j",
"extension_task_id": "mpb_mlpm1zv1_svwo4j",
"captcha_type": "HCAPTCHA",
"status": "processing",
"source": "mydisct_solver",
"trigger": "",
"tab_id": 7,
"frame_id": 0,
"page_url": "https://target-site.example/login",
"created_at": "2026-02-16T20:12:18.737Z",
"updated_at": "2026-02-16T20:12:22.119Z"
}
]
}
Returns a single task by id. Use this endpoint for deterministic per-task polling.
Example Request
curl "http://127.0.0.1:3000/mpb/v1/tasks/mpb_mlpm1zv1_svwo4j"
Completed Response Example
{
"ok": true,
"task": {
"task_id": "mpb_mlpm1zv1_svwo4j",
"extension_task_id": "mpb_mlpm1zv1_svwo4j",
"captcha_type": "HCAPTCHA",
"status": "completed",
"source": "mydisct_solver",
"trigger": "",
"tab_id": 7,
"frame_id": 0,
"page_url": "https://target-site.example/login",
"created_at": "2026-02-16T20:12:18.737Z",
"updated_at": "2026-02-16T20:12:43.379Z"
}
}
Task Lifecycle and Status Semantics
MPB task state is event-driven. The extension emits lifecycle transitions and bridge keeps the latest status.
Typical transition path is processing to completed. Failures end with failed.
| Status | Meaning | Action in Automation |
|---|---|---|
processing |
Extension detected captcha and solve flow is running | Keep polling task endpoint |
completed |
Solve flow finished successfully in extension context | Continue automation step |
failed |
Solve flow ended unsuccessfully | Retry or fallback strategy |
Timeline Example
[20:04:46] processing: mpb_mlplsb3l_etb204:HCAPTCHA
[20:04:56] completed: mpb_mlplsb3l_etb204:HCAPTCHA
Automation Integration Example (Node.js)
This example shows a practical task-based polling strategy. It fetches active processing tasks, then waits until each task becomes completed or failed.
const MPB_BASE = 'http://127.0.0.1:3000';
async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getProcessingTasks() {
const res = await fetch(MPB_BASE + '/mpb/v1/tasks?status=processing');
if (!res.ok) {
throw new Error('MPB tasks endpoint error: ' + res.status);
}
const data = await res.json();
return Array.isArray(data.tasks) ? data.tasks : [];
}
async function waitTask(taskId, timeoutMs) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
const res = await fetch(MPB_BASE + '/mpb/v1/tasks/' + encodeURIComponent(taskId));
if (res.status === 404) {
await sleep(500);
continue;
}
if (!res.ok) {
throw new Error('MPB task endpoint error: ' + res.status);
}
const data = await res.json();
const status = String(data.task && data.task.status || '').toLowerCase();
if (status === 'completed') {
return { ok: true, task: data.task };
}
if (status === 'failed') {
return { ok: false, reason: 'failed', task: data.task };
}
await sleep(700);
}
return { ok: false, reason: 'timeout' };
}
async function monitorLoop() {
const seen = new Set();
while (true) {
const tasks = await getProcessingTasks();
for (const task of tasks) {
const taskId = task.task_id;
if (!taskId || seen.has(taskId)) {
continue;
}
seen.add(taskId);
const result = await waitTask(taskId, 120000);
console.log('Task result:', taskId, result);
}
await sleep(1000);
}
}
monitorLoop().catch((error) => {
console.error('Monitor failed:', error.message);
process.exit(1);
});
Full Bridge Source (mpb-bridge-server.js)
The complete bridge implementation is included below.
const http = require('http');
const HOST = process.env.MPB_HOST || '127.0.0.1';
const PORT = Number(process.env.MPB_PORT || 3000);
let state = {
source: 'mpb-local-server',
mode: 'extension_live',
ts: Date.now(),
active_captchas: [],
events: []
};
const taskStore = new Map();
let lastStateDigest = '';
function sendJson(res, statusCode, payload) {
const body = JSON.stringify(payload);
res.writeHead(statusCode, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Private-Network': 'true',
'Content-Length': Buffer.byteLength(body)
});
res.end(body);
}
function readBody(req) {
return new Promise((resolve, reject) => {
let data = '';
req.on('data', (chunk) => {
data += chunk;
if (data.length > 1024 * 1024) {
reject(new Error('Payload too large'));
}
});
req.on('end', () => resolve(data));
req.on('error', reject);
});
}
function normalizeTaskId(item) {
const raw = item?.extension_task_id || item?.task_id || '';
return String(raw || '').trim();
}
function normalizeTaskStatus(status) {
return String(status || '').trim().toLowerCase();
}
function toIsoFromTs(ts) {
if (typeof ts === 'number' && Number.isFinite(ts) && ts > 0) {
return new Date(ts).toISOString();
}
return new Date().toISOString();
}
function ingestStateIntoTaskStore(snapshot) {
const activeCaptchas = Array.isArray(snapshot?.active_captchas) ? snapshot.active_captchas : [];
const events = Array.isArray(snapshot?.events) ? snapshot.events : [];
for (const item of activeCaptchas) {
const taskId = normalizeTaskId(item);
if (!taskId) {
continue;
}
const existing = taskStore.get(taskId) || {};
const nowIso = toIsoFromTs(snapshot?.ts);
taskStore.set(taskId, {
task_id: taskId,
extension_task_id: taskId,
captcha_type: item?.captcha_type || existing.captcha_type || '',
status: 'processing',
source: item?.source || snapshot?.source || existing.source || 'mydisct_solver',
trigger: item?.trigger || existing.trigger || '',
tab_id: item?.tab_id ?? existing.tab_id ?? -1,
frame_id: item?.frame_id ?? existing.frame_id ?? 0,
page_url: item?.page_url || existing.page_url || '',
created_at: existing.created_at || item?.created_at || nowIso,
updated_at: item?.updated_at || nowIso
});
}
for (const event of events) {
const taskId = normalizeTaskId(event);
if (!taskId) {
continue;
}
const existing = taskStore.get(taskId) || {};
const eventStatus = normalizeTaskStatus(event?.status) || existing.status || 'processing';
const eventIso = toIsoFromTs(event?.ts);
taskStore.set(taskId, {
task_id: taskId,
extension_task_id: taskId,
captcha_type: event?.captcha_type || existing.captcha_type || '',
status: eventStatus,
source: event?.source || existing.source || snapshot?.source || 'mydisct_solver',
trigger: event?.trigger || existing.trigger || '',
tab_id: event?.tab_id ?? existing.tab_id ?? -1,
frame_id: event?.frame_id ?? existing.frame_id ?? 0,
page_url: event?.page_url || existing.page_url || '',
created_at: existing.created_at || event?.started_at || eventIso,
updated_at: event?.ended_at || eventIso
});
}
}
function buildStateDigest(snapshot) {
const activeCaptchas = Array.isArray(snapshot?.active_captchas) ? snapshot.active_captchas : [];
const events = Array.isArray(snapshot?.events) ? snapshot.events : [];
const compactActive = activeCaptchas.map((item) => ({
id: normalizeTaskId(item),
type: item?.captcha_type || '',
status: item?.status || 'processing'
}));
const compactEvents = events.map((item) => ({
id: normalizeTaskId(item),
status: item?.status || '',
ts: item?.ts || 0
}));
return JSON.stringify({
source: snapshot?.source || '',
mode: snapshot?.mode || '',
active: compactActive,
events: compactEvents
});
}
function getTaskList(statusFilter, captchaTypeFilter) {
const normalizedStatus = normalizeTaskStatus(statusFilter);
const normalizedCaptcha = String(captchaTypeFilter || '').trim().toUpperCase();
const list = Array.from(taskStore.values()).filter((task) => {
if (normalizedStatus && normalizeTaskStatus(task.status) !== normalizedStatus) {
return false;
}
if (normalizedCaptcha && String(task.captcha_type || '').toUpperCase() !== normalizedCaptcha) {
return false;
}
return true;
});
list.sort((a, b) => {
const aTime = Date.parse(a.updated_at || a.created_at || 0) || 0;
const bTime = Date.parse(b.updated_at || b.created_at || 0) || 0;
return bTime - aTime;
});
return list;
}
const server = http.createServer(async (req, res) => {
const reqUrl = new URL(req.url, `http://${req.headers.host || `${HOST}:${PORT}`}`);
const path = reqUrl.pathname.replace(//+$/, '') || '/';
const now = new Date().toISOString();
if (req.method === 'OPTIONS') {
process.stdout.write(`[${now}] OPTIONS ${path}
`);
sendJson(res, 200, { ok: true });
return;
}
if (req.method === 'GET' && path === '/') {
process.stdout.write(`[${now}] GET /
`);
sendJson(res, 200, {
ok: true,
service: 'MyDisct Pulse Bridge',
endpoint: '/mpb/v1/active-captchas',
tasks_endpoint: '/mpb/v1/tasks',
task_status_endpoint: '/mpb/v1/tasks/:taskId'
});
return;
}
if (req.method === 'GET' && path === '/mpb/v1/active-captchas') {
process.stdout.write(`[${now}] GET /mpb/v1/active-captchas total=${state.active_captchas.length} events=${state.events.length}
`);
sendJson(res, 200, state);
return;
}
if (req.method === 'GET' && path === '/mpb/v1/tasks') {
const status = reqUrl.searchParams.get('status') || '';
const captchaType = reqUrl.searchParams.get('captcha_type') || '';
const tasks = getTaskList(status, captchaType);
process.stdout.write(`[${now}] GET /mpb/v1/tasks total=${tasks.length}
`);
sendJson(res, 200, {
ok: true,
total: tasks.length,
tasks
});
return;
}
if (req.method === 'GET' && path.startsWith('/mpb/v1/tasks/')) {
const taskId = decodeURIComponent(path.slice('/mpb/v1/tasks/'.length));
const task = taskStore.get(taskId);
if (!task) {
sendJson(res, 404, {
ok: false,
error: 'Task not found',
task_id: taskId
});
return;
}
process.stdout.write(`[${now}] GET /mpb/v1/tasks/${taskId} status=${task.status}
`);
sendJson(res, 200, {
ok: true,
task
});
return;
}
if (req.method === 'POST' && path === '/mpb/v1/active-captchas') {
try {
const bodyText = await readBody(req);
const payload = bodyText ? JSON.parse(bodyText) : {};
state = {
source: payload.source || 'mydisct_solver',
mode: payload.mode || 'extension_live',
ts: typeof payload.ts === 'number' ? payload.ts : Date.now(),
active_captchas: Array.isArray(payload.active_captchas) ? payload.active_captchas : [],
events: Array.isArray(payload.events) ? payload.events : []
};
ingestStateIntoTaskStore(state);
const digest = buildStateDigest(state);
const isChanged = digest !== lastStateDigest;
lastStateDigest = digest;
const preview = state.active_captchas
.slice(0, 5)
.map((item) => `${item?.extension_task_id || item?.task_id || '-'}:${item?.captcha_type || '-'}`)
.join(',');
const lastEvent = state.events.length > 0 ? state.events[state.events.length - 1] : null;
const eventPreview = lastEvent ? `${lastEvent.status || '-'}:${lastEvent.extension_task_id || lastEvent.task_id || '-'}:${lastEvent.captcha_type || '-'}` : '-';
if (isChanged) {
process.stdout.write(`[${now}] POST /mpb/v1/active-captchas source=${state.source} mode=${state.mode} total=${state.active_captchas.length} tasks=${preview} events=${state.events.length} last=${eventPreview}
`);
}
sendJson(res, 200, {
ok: true,
total_active: state.active_captchas.length,
total_events: state.events.length,
total_tasks: taskStore.size
});
return;
} catch (error) {
process.stdout.write(`[${now}] POST /mpb/v1/active-captchas error=${error.message}
`);
sendJson(res, 400, {
ok: false,
error: error.message
});
return;
}
}
process.stdout.write(`[${now}] ${req.method} ${path} 404
`);
sendJson(res, 404, {
ok: false,
error: 'Not found'
});
});
server.listen(PORT, HOST, () => {
process.stdout.write(`MPB bridge listening on http://${HOST}:${PORT}
`);
});
Best Practices
- Use
/mpb/v1/tasks/:taskIdpolling for deterministic task tracking instead of parsing console logs. - Keep polling interval between 500ms and 1000ms for responsive updates without unnecessary load.
- Treat
processingas transient state and always wait forcompletedorfailed. - Filter by
captcha_typewhen your automation targets specific captcha families. - Persist task IDs in your automation layer if you need auditability beyond bridge process lifetime.
Troubleshooting
Bridge process is not running or URL is incorrect. Start bridge first, then verify extension MPB URL and port match exactly.
Zero active means extension currently has no processing task in snapshot. Confirm captcha module is enabled, captcha is detected on page, and extension is in active solving flow.
Repeated posts are normal heartbeat behavior. For stable integration, use task-based polling endpoints instead of relying on console frequency.