import { z } from 'zod' import { resolveAsset } from '../refs.js' import { assetTypeSchema } from '../schemas.js' import type { MarketV1Client } from '../v1/client.js' import { base64, result } from './result.js' import type { MarketMcpServer } from './types.js' const searchMarketSchema = z .object({ query: z.string().min(1).max(200), type: assetTypeSchema, limit: z.coerce.number().int().min(1).max(5).default(3), includeUnapproved: z.boolean().default(false), }) .strict() const assetRefSchema = z.string().min(1).describe('Asset name or exact name@version') const assetInput = z .object({ asset: assetRefSchema, includeUnapproved: z.boolean().default(false), key: z.string().min(1).max(128).optional(), }) .strict() export function registerAssetTools( server: MarketMcpServer, client: MarketV1Client, fetcher: typeof globalThis.fetch, ): void { server.registerTool( 'list_market_asset_types', { title: 'List Market asset types', description: 'List current Drawcall Market asset types with their search, generation, and installation guidance.', annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true, idempotentHint: true, }, }, async () => result({ types: await client.asset.installMetadata() }), ) server.registerTool( 'search_market', { title: 'Search Market', description: 'Search Drawcall Market for assets matching one concrete need. Returns exact name@version references; call get_market_asset before selecting one.', inputSchema: searchMarketSchema, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true, idempotentHint: true, }, }, async ({ query, type, limit, includeUnapproved }) => { const response = await client.asset.search({ query, type, limit, page: 1, includeUnapproved, }) if (!('items' in response)) { throw new Error('Market search unexpectedly returned an exact asset result') } return result({ assets: response.items.map((asset) => ({ asset: `${asset.name}@${asset.latestVersion}`, name: asset.name, version: asset.latestVersion, type: asset.type, description: asset.description, approved: asset.approved, access: asset.access, previewUrl: asset.previewUrl, })), total: response.total, }) }, ) server.registerTool( 'get_market_asset', { title: 'Get Market asset', description: 'Get one exact Drawcall Market asset manifest with its version history, dependencies, files, preview URL, and zip URL.', inputSchema: assetInput, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true, idempotentHint: true, }, }, async ({ asset, includeUnapproved, key }) => result({ asset: await resolveAsset(client.asset, asset, { includeUnapproved, ...(key ? { key } : {}), }), }), ) server.registerTool( 'get_market_asset_preview', { title: 'Get Market asset preview', description: 'Return the preview image for one exact Drawcall Market asset.', inputSchema: assetInput, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true, idempotentHint: true, }, }, async ({ asset: ref, includeUnapproved, key }) => { const asset = await resolveAsset(client.asset, ref, { includeUnapproved, ...(key ? { key } : {}), }) if (!asset.previewUrl) throw new Error(`Asset "${ref}" has no preview image`) const response = await fetcher(asset.previewUrl) if (!response.ok) { throw new Error( `Could not read preview ${asset.previewUrl}: ${response.status} ${response.statusText}`, ) } const mimeType = response.headers.get('content-type')?.split(';', 1)[0] if (!mimeType?.startsWith('image/')) { throw new Error(`Preview ${asset.previewUrl} returned ${mimeType ?? 'no media type'}`) } return { content: [ { type: 'image', data: base64(new Uint8Array(await response.arrayBuffer())), mimeType, }, ], structuredContent: { asset: `${asset.name}@${asset.version}`, url: asset.previewUrl, mediaType: mimeType, }, } }, ) }