/ utils / dxt / helpers.ts
helpers.ts
 1  import type { McpbManifest } from '@anthropic-ai/mcpb'
 2  import { errorMessage } from '../errors.js'
 3  import { jsonParse } from '../slowOperations.js'
 4  
 5  /**
 6   * Parses and validates a DXT manifest from a JSON object.
 7   *
 8   * Lazy-imports @anthropic-ai/mcpb: that package uses zod v3 which eagerly
 9   * creates 24 .bind(this) closures per schema instance (~300 instances between
10   * schemas.js and schemas-loose.js). Deferring the import keeps ~700KB of bound
11   * closures out of the startup heap for sessions that never touch .dxt/.mcpb.
12   */
13  export async function validateManifest(
14    manifestJson: unknown,
15  ): Promise<McpbManifest> {
16    const { McpbManifestSchema } = await import('@anthropic-ai/mcpb')
17    const parseResult = McpbManifestSchema.safeParse(manifestJson)
18  
19    if (!parseResult.success) {
20      const errors = parseResult.error.flatten()
21      const errorMessages = [
22        ...Object.entries(errors.fieldErrors).map(
23          ([field, errs]) => `${field}: ${errs?.join(', ')}`,
24        ),
25        ...(errors.formErrors || []),
26      ]
27        .filter(Boolean)
28        .join('; ')
29  
30      throw new Error(`Invalid manifest: ${errorMessages}`)
31    }
32  
33    return parseResult.data
34  }
35  
36  /**
37   * Parses and validates a DXT manifest from raw text data.
38   */
39  export async function parseAndValidateManifestFromText(
40    manifestText: string,
41  ): Promise<McpbManifest> {
42    let manifestJson: unknown
43  
44    try {
45      manifestJson = jsonParse(manifestText)
46    } catch (error) {
47      throw new Error(`Invalid JSON in manifest.json: ${errorMessage(error)}`)
48    }
49  
50    return validateManifest(manifestJson)
51  }
52  
53  /**
54   * Parses and validates a DXT manifest from raw binary data.
55   */
56  export async function parseAndValidateManifestFromBytes(
57    manifestData: Uint8Array,
58  ): Promise<McpbManifest> {
59    const manifestText = new TextDecoder().decode(manifestData)
60    return parseAndValidateManifestFromText(manifestText)
61  }
62  
63  /**
64   * Generates an extension ID from author name and extension name.
65   * Uses the same algorithm as the directory backend for consistency.
66   */
67  export function generateExtensionId(
68    manifest: McpbManifest,
69    prefix?: 'local.unpacked' | 'local.dxt',
70  ): string {
71    const sanitize = (str: string) =>
72      str
73        .toLowerCase()
74        .replace(/\s+/g, '-')
75        .replace(/[^a-z0-9-_.]/g, '')
76        .replace(/-+/g, '-')
77        .replace(/^-+|-+$/g, '')
78  
79    const authorName = manifest.author.name
80    const extensionName = manifest.name
81  
82    const sanitizedAuthor = sanitize(authorName)
83    const sanitizedName = sanitize(extensionName)
84  
85    return prefix
86      ? `${prefix}.${sanitizedAuthor}.${sanitizedName}`
87      : `${sanitizedAuthor}.${sanitizedName}`
88  }