* Extract MCPB file and write contents to extraction directory. * * @param modes - name→mode map from `parseZipModes`. MCPB bundles can ship * native MCP server binaries, so preserving the exec bit matters here.
( unzipped: Record<string, Uint8Array>, extractPath: string, modes: Record<string, number>, onProgress?: ProgressCallback, )
| 548 | * native MCP server binaries, so preserving the exec bit matters here. |
| 549 | */ |
| 550 | async function extractMcpbContents( |
| 551 | unzipped: Record<string, Uint8Array>, |
| 552 | extractPath: string, |
| 553 | modes: Record<string, number>, |
| 554 | onProgress?: ProgressCallback, |
| 555 | ): Promise<void> { |
| 556 | if (onProgress) { |
| 557 | onProgress('Extracting files...') |
| 558 | } |
| 559 | |
| 560 | // Create extraction directory |
| 561 | await getFsImplementation().mkdir(extractPath) |
| 562 | |
| 563 | // Write all files. Filter directory entries from the count so progress |
| 564 | // messages use the same denominator as filesWritten (which skips them). |
| 565 | let filesWritten = 0 |
| 566 | const entries = Object.entries(unzipped).filter(([k]) => !k.endsWith('/')) |
| 567 | const totalFiles = entries.length |
| 568 | |
| 569 | for (const [filePath, fileData] of entries) { |
| 570 | // Directory entries (common in zip -r, Python zipfile, Java ZipOutputStream) |
| 571 | // are filtered above — writeFile would create `bin/` as an empty regular |
| 572 | // file, then mkdir for `bin/server` would fail with ENOTDIR. The |
| 573 | // mkdir(dirname(fullPath)) below creates parent dirs implicitly. |
| 574 | |
| 575 | const fullPath = join(extractPath, filePath) |
| 576 | const dir = dirname(fullPath) |
| 577 | |
| 578 | // Ensure directory exists (recursive handles already-existing) |
| 579 | if (dir !== extractPath) { |
| 580 | await getFsImplementation().mkdir(dir) |
| 581 | } |
| 582 | |
| 583 | // Determine if text or binary |
| 584 | const isTextFile = |
| 585 | filePath.endsWith('.json') || |
| 586 | filePath.endsWith('.js') || |
| 587 | filePath.endsWith('.ts') || |
| 588 | filePath.endsWith('.txt') || |
| 589 | filePath.endsWith('.md') || |
| 590 | filePath.endsWith('.yml') || |
| 591 | filePath.endsWith('.yaml') |
| 592 | |
| 593 | if (isTextFile) { |
| 594 | const content = new TextDecoder().decode(fileData) |
| 595 | await writeFile(fullPath, content, 'utf-8') |
| 596 | } else { |
| 597 | await writeFile(fullPath, Buffer.from(fileData)) |
| 598 | } |
| 599 | |
| 600 | const mode = modes[filePath] |
| 601 | if (mode && mode & 0o111) { |
| 602 | // Swallow EPERM/ENOTSUP (NFS root_squash, some FUSE mounts) — losing +x |
| 603 | // is the pre-PR behavior and better than aborting mid-extraction. |
| 604 | await chmod(fullPath, mode & 0o777).catch(() => {}) |
| 605 | } |
| 606 | |
| 607 | filesWritten++ |
no test coverage detected