81 lines
2.0 KiB
TypeScript
81 lines
2.0 KiB
TypeScript
/**
|
|
* MCP (`d2cs` Realm Server, Port 6113) stream framing (`u16 len | u8 id`).
|
|
* Verified against `D2MCPClient.dll` (1.13c). Client sends 1-byte greeting `0x01` on connect.
|
|
*/
|
|
|
|
import { ProtocolError } from '../core/errors.ts'
|
|
|
|
export const MCP_GREETING_BYTE = 0x01
|
|
|
|
export interface McpRawPacket {
|
|
readonly id: number
|
|
readonly payload: Uint8Array
|
|
readonly raw: Uint8Array
|
|
}
|
|
|
|
export function encodeMcpFrame(packetId: number, payload: Uint8Array): Uint8Array {
|
|
const totalLen = 3 + payload.length
|
|
if (totalLen > 0xffff) {
|
|
throw new ProtocolError('realm', packetId, 0, `MCP packet too large (${totalLen}B)`)
|
|
}
|
|
const out = new Uint8Array(totalLen)
|
|
out[0] = totalLen & 0xff
|
|
out[1] = (totalLen >>> 8) & 0xff
|
|
out[2] = packetId & 0xff
|
|
out.set(payload, 3)
|
|
return out
|
|
}
|
|
|
|
export class McpFramer {
|
|
private buf: Uint8Array = new Uint8Array(0)
|
|
|
|
push(chunk: Uint8Array): McpRawPacket[] {
|
|
if (chunk.length === 0) return []
|
|
if (this.buf.length === 0) {
|
|
this.buf = chunk.slice()
|
|
} else {
|
|
const merged = new Uint8Array(this.buf.length + chunk.length)
|
|
merged.set(this.buf, 0)
|
|
merged.set(chunk, this.buf.length)
|
|
this.buf = merged
|
|
}
|
|
|
|
const packets: McpRawPacket[] = []
|
|
let offset = 0
|
|
|
|
while (this.buf.length - offset >= 3) {
|
|
const totalLen = this.buf[offset]! | (this.buf[offset + 1]! << 8)
|
|
const id = this.buf[offset + 2]!
|
|
if (totalLen < 3) {
|
|
throw new ProtocolError(
|
|
'realm',
|
|
id,
|
|
offset,
|
|
`Invalid MCP frame length ${totalLen} (< 3)`,
|
|
this.buf.slice(offset),
|
|
)
|
|
}
|
|
if (this.buf.length - offset < totalLen) {
|
|
break
|
|
}
|
|
const raw = this.buf.slice(offset, offset + totalLen)
|
|
const payload = raw.subarray(3)
|
|
packets.push({ id, payload, raw })
|
|
offset += totalLen
|
|
}
|
|
|
|
if (offset > 0) {
|
|
this.buf = this.buf.slice(offset)
|
|
}
|
|
return packets
|
|
}
|
|
|
|
get bufferedBytes(): number {
|
|
return this.buf.length
|
|
}
|
|
|
|
reset(): void {
|
|
this.buf = new Uint8Array(0)
|
|
}
|
|
}
|