English | 简体中文
MDXP (Motrix Download eXchange Protocol) — the JSON-RPC 2.0 wire types, Zod schemas, and bidirectional connection helper that let a browser, CLI, or AI agent hand downloads to a Motrix desktop downloader over any duplex transport.
@motrix/mdxp is the single source of truth for the MDXP wire contract. Both
sides of the bridge — the Motrix desktop app
(the server, which owns the download engine) and its clients (the
browser extension, a CLI, or an agent) — depend on this package so the protocol
shape is defined exactly once.
It ships nothing transport-specific: you bring any
vscode-jsonrpc
MessageReader/MessageWriter pair (stdio, a socket, a WebSocket, a
MessagePort) and the library builds a fully typed, bidirectional connection on
top of it.
- Schema-first. Every wire shape is a Zod schema; the
TypeScript types are
z.inferof those schemas, so validation and types can never drift apart. - Fully typed connection.
sendRequest/onRequest/sendNotification/onNotificationare generic over the method name — params and result types are inferred from that name, with no casts at the call site. - Transport-agnostic. Works over anything that implements
MessageReader/MessageWriter. - Platform RAL entry points.
./nodeand./browserinstall the matchingvscode-jsonrpcruntime abstraction layer and re-export its transport classes, so you import everything — connection helper and reader/writer — from one place. - One-call constructors.
fromWebSocket,fromStdio, andfromWorkerbuild a connection over a common transport in a single call;createMdxpConnectionstays the generic escape hatch. - Agent-ready. A built-in tool registry emits a JSON-Schema tool catalog you can feed straight into an LLM function-calling API.
- Forward-compatible. Unknown methods and fields are ignored, not rejected.
npm install @motrix/mdxp
# or: pnpm add @motrix/mdxp · yarn add @motrix/mdxpRuntime dependency: vscode-jsonrpc
^9, installed alongside this package. Its transport classes (reader/writer)
and the primitives that surface in this package's API — MessageReader,
MessageConnection, CancellationToken, CancellationTokenSource, … — are
re-exported from @motrix/mdxp (see Entry points), so you
rarely need to import vscode-jsonrpc directly. ESM-only; requires
Node.js ≥ 18 or a modern bundler.
| Import | Installs a RAL? | Use it from |
|---|---|---|
@motrix/mdxp |
No — platform-agnostic core | Shared code, tests, type-only imports |
@motrix/mdxp/node |
Node RAL | A Node host (Electron main, a CLI, a native-messaging host) |
@motrix/mdxp/browser |
Browser RAL | A browser host (extension service worker, page) |
vscode-jsonrpc v9 requires a runtime abstraction layer (RAL) to be installed
before a connection can be created. Importing @motrix/mdxp/node or
@motrix/mdxp/browser installs the right one into the same vscode-jsonrpc
instance this package uses, and re-exports the entire public API plus that
platform's transport classes (StreamMessageReader/Writer for Node,
BrowserMessageReader/Writer for the browser) — so a host imports everything,
including its reader/writer, from a single place.
import { fromStdio } from '@motrix/mdxp/node'
// Over process.stdin / process.stdout (the native-messaging / CLI case).
const conn = fromStdio()
// Register handlers BEFORE listen().
conn.onNotification('$/task/progress', (p) => {
const pct = p.bytesTotal ? Math.round((p.bytesDone / p.bytesTotal) * 100) : null
console.log(`[${p.taskId}] ${p.phase} ${pct ?? '?'}% @ ${p.speedBps} B/s`)
})
conn.listen()import { fromWebSocket } from '@motrix/mdxp/browser'
const conn = fromWebSocket(new WebSocket('ws://127.0.0.1:16650/v1'))
conn.onNotification('$/task/progress', (p) => {})
conn.listen()createMdxpConnection(reader, writer) is the generic entry point — bring any
vscode-jsonrpc reader/writer. For the common transports, skip the boilerplate:
| Constructor | Entry | Transport |
|---|---|---|
fromWebSocket(ws) |
./node · ./browser |
A browser WebSocket or a Node ws socket |
fromStdio(opts?) |
./node |
process.stdin / process.stdout, or given streams |
fromWorker(port) |
./browser |
A Worker or MessagePort |
Each returns a ready MdxpConnection — you still register handlers and call
listen(). For any other transport, build the reader/writer and call
createMdxpConnection directly.
Server vs. client. The Motrix desktop app is the server — it owns the download engine. A client is whatever drives it: the browser extension, a CLI, or an agent. The connection is symmetric, but methods flow in a defined direction (below).
The handshake comes first. motrix/initialize MUST be the first message of
every session. It negotiates the protocol version, exchanges identity, and
declares capabilities. Nothing else should be sent until it resolves.
Message direction. Most methods are client→server (the client asks the
downloader to do something). Two are server→client — the server asks the client
to inspect a page: url/probe and url/resolve (see
SERVER_INITIATED_METHODS). Because the server initiates both,
a client answers them with onRequest, while the server side calls them with
sendRequest.
const hello = await conn.sendRequest('motrix/initialize', {
protocolVersion: '1.0',
client: {
kind: 'cli', // or 'extension'
name: 'my-download-agent',
version: '1.0.0',
locale: 'en-US',
},
capabilities: { submitDownload: true, progress: true, cancellation: true },
adapters: [], // page adapters this client can resolve, if any
})
console.log(hello.server.name, hello.server.version)
console.log(hello.capabilities.selectionKinds) // e.g. ['direct', 'hls', 'mux']download/add is the public, agent-facing entry point. It accepts a direct
URL list, a magnet link, or a base64 torrent, and returns the created task
snapshot so you can render it without polling.
// Direct HTTP(S) file
const task = await conn.sendRequest('download/add', {
kind: 'url',
saveDir: '/Users/me/Downloads',
uris: ['https://cdn.example.com/releases/app-1.4.2-arm64.dmg'],
connections: 8,
})
console.log(task.id, task.status) // "t_01H…", "downloading"
// Magnet link
await conn.sendRequest('download/add', {
kind: 'magnet',
saveDir: '/Users/me/Downloads',
uri: 'magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a',
})Only
http,https,ftp,ftps, andsftpURLs are accepted — the schema rejectsfile:,data:, andjavascript:at the contract boundary, so an agent can never be coerced into a local-file read.
const { tasks, total } = await conn.sendRequest('task/list', {
status: 'downloading',
limit: 20,
})
await conn.sendRequest('task/pause', { taskId: task.id })
await conn.sendRequest('task/resume', { taskId: task.id })
await conn.sendRequest('task/remove', { taskId: task.id, deleteFiles: false })The desktop app asks a client whether it can handle a page (url/probe), then
asks it to extract the downloadable resources (url/resolve). A client answers
by registering handlers:
conn.onRequest('url/probe', async ({ url }) => ({
handled: /videos\.example\.com/.test(url),
adapterId: 'example-video',
confidence: 'high',
}))
conn.onRequest('url/resolve', async ({ url, preferences }) => ({
selections: [
{
kind: 'direct',
primary: {
url: 'https://cdn.example.com/v/abc123/1080p.mp4',
headers: {},
cookies: [],
refererPolicy: 'strict-origin-when-cross-origin',
},
container: 'mp4',
quality: preferences?.maxQuality ?? '1080p',
sizeBytes: 734_003_200,
},
],
meta: { title: 'Sample clip', author: 'example.com', durationSec: 372 },
extractedBy: {
adapterId: 'example-video',
adapterVersion: '1.0.0',
extractedAt: Date.now(),
},
}))A selection is a discriminated union on kind: direct (one file), hls
(a playlist), or mux (separate video + audio streams the server muxes). Each
Resource carries the headers/cookies needed to re-fetch it server-side.
conn.onNotification('$/task/progress', (p) => {
// p.phase: 'queued' | 'downloading' | 'muxing' | 'finalizing'
})
conn.onNotification('$/task/completed', (p) => {
console.log('done →', p.filePath, `(${p.durationMs} ms)`)
})
conn.onNotification('$/task/error', (p) => {
console.error(`task ${p.taskId} failed: [${p.code}] ${p.message}`)
})sendRequest accepts an optional CancellationToken. Cancelling emits
$/cancelRequest on the wire (handled by vscode-jsonrpc); a cooperative
handler observes token.isCancellationRequested.
import { CancellationTokenSource } from '@motrix/mdxp'
const cts = new CancellationTokenSource()
const pending = conn.sendRequest('url/resolve', { url }, cts.token)
// …the user navigated away:
cts.cancel()Every wire shape has a schema. Validate untrusted input at your boundary with
safeParse before acting on it:
import { DownloadAddParamsSchema } from '@motrix/mdxp'
const parsed = DownloadAddParamsSchema.safeParse(untrusted)
if (!parsed.success) {
// parsed.error — a ZodError describing exactly what was wrong
return
}
await conn.sendRequest('download/add', parsed.data)Return structured errors from a handler with makeMdxpError. The code is a
JSON-RPC error code; data carries a machine-readable appCode, a retry hint,
and free-form context.
import { ErrorCodes, makeMdxpError } from '@motrix/mdxp'
throw makeMdxpError(
ErrorCodes.ResourceUnavailable,
'The requested file is no longer available',
{ appCode: 'http.gone', retryable: false, context: { status: 410 } },
)Classify a received code with isProtocolError(code) (JSON-RPC reserved) or
isMotrixError(code) (Motrix's -32001…-32099 range).
The agent-facing methods are exposed as a JSON-Schema tool catalog, ready for an LLM function-calling / tool-use API:
import { toAgentToolCatalog } from '@motrix/mdxp'
const tools = toAgentToolCatalog()
// [
// { name: 'download/add', description, inputSchema: {…JSON Schema}, outputSchema },
// { name: 'task/list', … },
// …
// ]| Export | Kind | Purpose |
|---|---|---|
createMdxpConnection(reader, writer) |
function | Wrap a reader/writer pair in a typed MdxpConnection. |
fromWebSocket · fromStdio · fromWorker |
function | One-call constructors over a WebSocket / stdio / Worker (from ./node · ./browser). |
MdxpConnection |
type | The connection interface (sendRequest, onRequest, sendNotification, onNotification, dispose, raw). |
MdxpRequestMap / MdxpNotificationMap |
type | Method/notification name → params/result type maps. |
Methods / Notifications |
const | Wire-name constants (Methods.DownloadAdd === 'download/add'). |
ErrorCodes |
const | JSON-RPC + Motrix-defined error codes. |
makeMdxpError(code, msg, data?) |
function | Build a structured MdxpError. |
isProtocolError / isMotrixError |
function | Classify an error code. |
Tools |
const | Registry of every client→server method → { description, paramsSchema, resultSchema, agentFacing }. |
toAgentToolCatalog() |
function | The agentFacing subset as JSON-Schema tools. |
SERVER_INITIATED_METHODS |
const | Methods the server calls on the client (url/probe, url/resolve). |
*Schema |
Zod schema | Every wire shape, for runtime validation. |
MessageReader · MessageWriter · MessageConnection · CancellationToken · CancellationTokenSource · Disposable |
re-export | vscode-jsonrpc primitives used across the API. Platform transport classes (StreamMessageReader/Writer, BrowserMessageReader/Writer) are re-exported from ./node and ./browser. |
| Method | Direction | Agent-facing | Purpose |
|---|---|---|---|
motrix/initialize |
client → server | Handshake: version, identity, capabilities. | |
system/ping |
client → server | Liveness probe; echoes sentAt with recvAt. |
|
download/submit |
client → server | Submit a browser-detected, page-shaped download. | |
download/cancel |
client → server | Cancel a submitted download by task id. | |
download/add |
client → server | ✓ | Add a download by URL(s), magnet, or torrent. |
task/list |
client → server | ✓ | List tasks, filterable + paginated. |
task/get |
client → server | ✓ | Get one task by id. |
task/pause · task/resume |
client → server | ✓ | Pause / resume a task. |
task/remove |
client → server | ✓ | Remove a task, optionally deleting files. |
stats/get |
client → server | ✓ | Aggregate global stats (speeds + counts). |
engine/status |
client → server | ✓ | Engine lifecycle state + feature report. |
url/probe |
server → client | Can this client's adapters handle a page? | |
url/resolve |
server → client | Extract downloadable resources from a page. |
| Notification | Direction | Payload |
|---|---|---|
motrix/initialized |
client → server | Handshake completion (no payload). |
$/task/progress |
server → client | bytesDone, bytesTotal, speedBps, etaSec, phase. |
$/task/completed |
server → client | filePath, durationMs. |
$/task/error |
server → client | code, message. |
$/stats |
server → client | Periodic aggregate stats push. |
$/pair/revoked |
server → client | Pairing was revoked (reason). |
$/cancelRequest |
either | Cancellation — handled by vscode-jsonrpc. |
| Code | Value | Range |
|---|---|---|
ParseError |
-32700 |
JSON-RPC reserved |
InvalidRequest |
-32600 |
JSON-RPC reserved |
MethodNotFound |
-32601 |
JSON-RPC reserved |
InvalidParams |
-32602 |
JSON-RPC reserved |
InternalError |
-32603 |
JSON-RPC reserved |
RequestCancelled |
-32800 |
LSP extension |
AdapterError |
-32001 |
Motrix |
ResourceUnavailable |
-32002 |
Motrix |
PermissionDenied |
-32003 |
Motrix |
RateLimited |
-32004 |
Motrix |
CapabilityNotSupported |
-32005 |
Motrix |
PairRevoked |
-32006 |
Motrix |
- Version.
protocolVersionis'1.0'. This is the wire-compatibility version and is independent of this package's npm version. - No batching. JSON-RPC batching is forbidden — one frame, one message.
- Forward-compatible. Result and notification payloads are non-strict:
a newer server may add fields that older clients ignore. An unknown method is
rejected with
MethodNotFoundrather than crashing the session.
- Transport-agnostic — the library never assumes a specific transport; any
MessageReader/MessageWriterduplex works. - Schema-first — define the Zod schema, infer the type; never hand-write a type that has a corresponding schema.
- Forward-compatible — ignore the unknown rather than reject it.
MIT © Dr_rOot