#
tokens: 11846/50000 12/12 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .github
│   └── workflows
│       └── publish.yml
├── .gitignore
├── .npmrc
├── .zed
│   └── settings.json
├── biome.json
├── CLAUDE.md
├── package.json
├── pnpm-lock.yaml
├── README.md
├── src
│   ├── __tests__
│   │   ├── index.test.ts
│   │   ├── test-output-128x128.png
│   │   └── test-output-64x64.png
│   ├── index.ts
│   └── utils.ts
├── tsconfig.json
└── tsup.config.ts
```

# Files

--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------

```
1 | registry=https://registry.npmmirror.com/
2 | # registry=https://registry.npmjs.org/
3 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Logs
 2 | logs
 3 | *.log
 4 | npm-debug.log*
 5 | yarn-debug.log*
 6 | yarn-error.log*
 7 | pnpm-debug.log*
 8 | lerna-debug.log*
 9 | 
10 | node_modules
11 | .output
12 | stats.html
13 | stats-*.json
14 | .wxt
15 | web-ext.config.ts
16 | 
17 | # Editor directories and files
18 | .vscode/*
19 | !.vscode/extensions.json
20 | .idea
21 | .DS_Store
22 | *.suo
23 | *.ntvs*
24 | *.njsproj
25 | *.sln
26 | *.sw?
27 | 
28 | .env
29 | 
30 | dist
31 | 
```

--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------

```markdown
 1 | # CLAUDE.md
 2 | 
 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
 4 | 
 5 | ## Development Commands
 6 | 
 7 | - `npm run build` - Build the project using tsup
 8 | - `npm run dev` - Build in watch mode for development
 9 | - `npm test` - Run tests using Vitest
10 | - `npm run preview` - Run the built MCP server locally
11 | - `npm run build:tsc` - Build with TypeScript compiler directly
12 | 
13 | ## Project Architecture
14 | 
15 | This is an MCP (Model Context Protocol) server that provides file upload functionality. The architecture is straightforward:
16 | 
17 | ### Core Components
18 | 
19 | - **Main Server** (`src/index.ts`): Single-file MCP server using `@modelcontextprotocol/sdk`
20 | - **Upload Handler** (`uploadFileHandler`): Handles both local file paths and URLs, converts files to FormData for upload
21 | - **Environment Configuration**: Uses env vars for upload endpoint configuration
22 | 
23 | ### Key Implementation Details
24 | 
25 | - Uses `undici` for HTTP requests and FormData handling
26 | - Supports both local file paths (including `file://` URIs) and HTTP(S) URLs as input
27 | - Converts Buffer to Blob via `new Blob([new Uint8Array(buffer)])` for FormData compatibility
28 | - Environment variables control upload behavior:
29 |   - `UPLOAD_URL`: Target upload endpoint (required)
30 |   - `FILE_KEY`: Form field name for the file (required) 
31 |   - `FILE_NAME`: Form field name for filename (required)
32 |   - `EXTRA_FORM`: Additional form fields as JSON string (optional)
33 | 
34 | ### Testing Strategy
35 | 
36 | - Uses Vitest for testing
37 | - Mocks `undici` and `FormData` for upload testing
38 | - Tests file URI handling, Buffer-to-Blob conversion, and error cases
39 | - Sets `SKIP_MCP_MAIN=1` to prevent CLI execution during tests
40 | 
41 | ### Code Quality
42 | 
43 | - Uses Biome for linting and formatting with tabs, 80-char line width
44 | - TypeScript with Node16 module resolution and strict mode
45 | - ESM modules throughout
```

--------------------------------------------------------------------------------
/.zed/settings.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "formatter": {
 3 |     "language_server": {
 4 |       "name": "biome"
 5 |     }
 6 |   },
 7 |   "format_on_save": "on",
 8 |   "code_actions_on_format": {
 9 |     "source.fixAll.biome": true,
10 |     "source.organizeImports.biome": true
11 |   }
12 | }
13 | 
```

--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Publish
 2 | on:
 3 |   release:
 4 |     types: [created]
 5 | jobs:
 6 |   build:
 7 |     runs-on: ubuntu-latest
 8 |     steps:
 9 |       - uses: actions/checkout@v3
10 |       - uses: actions/setup-node@v3
11 |         with:
12 |           node-version: '20.x'
13 |           registry-url: 'https://registry.npmjs.org'
14 |       - run: npm install 
15 |       - run: npm run build
16 |       - run: npm run release
17 |         env:
18 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```

--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { defineConfig } from "tsup"
 2 | 
 3 | export default defineConfig({
 4 | 	entry: ["src/index.ts"],
 5 | 	format: ["esm"],
 6 | 	dts: true,
 7 | 	splitting: false,
 8 | 	sourcemap: false,
 9 | 	clean: true,
10 | 	minify: false,
11 | 	outDir: "dist",
12 | 	// Ensure all dependencies are bundled properly
13 | 	noExternal: ["zod", "zod-to-json-schema"],
14 | 	// Bundle all dependencies to avoid module resolution issues
15 | 	bundle: true,
16 | 	// Ensure proper platform compatibility
17 | 	platform: "node",
18 | 	target: "node18",
19 | })
20 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"name": "mcp-upload-file",
 3 | 	"version": "1.0.12",
 4 | 	"bin": {
 5 | 		"mcp-upload-file": "dist/index.js"
 6 | 	},
 7 | 	"description": "mcp upload file",
 8 | 	"type": "module",
 9 | 	"main": "./dist/index.js",
10 | 	"module": "./dist/index.js",
11 | 	"types": "./dist/index.d.ts",
12 | 	"exports": {
13 | 		".": "./dist/index.js"
14 | 	},
15 | 	"files": [
16 | 		"dist",
17 | 		"*.d.ts"
18 | 	],
19 | 	"scripts": {
20 | 		"build": "tsup",
21 | 		"release": "npm publish --no-git-checks --access public --registry https://registry.npmjs.org/",
22 | 		"dev": "tsup --watch",
23 | 		"preview": "node dist/index.js",
24 | 		"test": "vitest",
25 | 		"build:tsc": "tsc"
26 | 	},
27 | 	"keywords": [
28 | 		"mcp",
29 | 		"upload file"
30 | 	],
31 | 	"author": "hens",
32 | 	"license": "ISC",
33 | 	"devDependencies": {
34 | 		"@biomejs/biome": "^1.9.4",
35 | 		"@types/node": "^22.13.4",
36 | 		"tsup": "^8.3.6",
37 | 		"typescript": "^5.7.3",
38 | 		"vitest": "^3.0.6"
39 | 	},
40 | 	"dependencies": {
41 | 		"@modelcontextprotocol/sdk": "^1.7.0",
42 | 		"sharp": "^0.34.3",
43 | 		"undici": "^7.3.0",
44 | 		"zod": "^3.24.2",
45 | 		"zod-to-json-schema": "^3.24.2"
46 | 	}
47 | }
48 | 
```

--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
 3 |   "organizeImports": {
 4 |     "enabled": true
 5 |   },
 6 |   "linter": {
 7 |     "enabled": true,
 8 |     "rules": {
 9 |       "recommended": true,
10 |       "suspicious": {
11 |         "noExplicitAny": "off"
12 |       },
13 |       "correctness": {
14 |         "noUnusedVariables": "warn",
15 |         "noUnusedImports": "warn"
16 |       },
17 |       "style": {
18 |         "noParameterAssign": "off"
19 |       },
20 |       "a11y": {
21 |         "useKeyWithClickEvents": "off"
22 |       }
23 |     }
24 |   },
25 |   "formatter": {
26 |     "enabled": true,
27 |     "formatWithErrors": false,
28 |     "ignore": [],
29 |     "attributePosition": "auto",
30 |     "indentStyle": "tab",
31 |     "indentWidth": 2,
32 |     "lineWidth": 80,
33 |     "lineEnding": "lf"
34 |   },
35 |   "javascript": {
36 |     "formatter": {
37 |       "arrowParentheses": "always",
38 |       "bracketSameLine": false,
39 |       "bracketSpacing": true,
40 |       "jsxQuoteStyle": "double",
41 |       "quoteProperties": "asNeeded",
42 |       "semicolons": "asNeeded",
43 |       "trailingCommas": "all"
44 |     }
45 |   },
46 |   "json": {
47 |     "formatter": {
48 |       "trailingCommas": "none"
49 |     }
50 |   }
51 | }
52 | 
```

--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { request } from "undici"
  2 | import fs from "node:fs"
  3 | import sharp from "sharp"
  4 | 
  5 | export interface FileSource {
  6 | 	buffer: Buffer
  7 | 	resolvedPath?: string
  8 | }
  9 | 
 10 | export function validateEnvironmentVariables(): string | null {
 11 | 	if (
 12 | 		!process.env.UPLOAD_URL ||
 13 | 		!process.env.FILE_KEY ||
 14 | 		!process.env.FILE_NAME
 15 | 	) {
 16 | 		return "Missing required environment variables: UPLOAD_URL, FILE_KEY, FILE_NAME"
 17 | 	}
 18 | 	return null
 19 | }
 20 | 
 21 | export function isHttpUrl(source: string): boolean {
 22 | 	return source.startsWith("http://") || source.startsWith("https://")
 23 | }
 24 | 
 25 | export function parseFileUri(source: string): string {
 26 | 	if (!source.startsWith("file://")) {
 27 | 		return source
 28 | 	}
 29 | 
 30 | 	try {
 31 | 		return decodeURIComponent(new URL(source).pathname)
 32 | 	} catch {
 33 | 		return source.replace(/^file:\/\//, "")
 34 | 	}
 35 | }
 36 | 
 37 | export async function fetchFileFromUrl(url: string): Promise<Buffer> {
 38 | 	const response = await request(url)
 39 | 	return Buffer.from(await response.body.arrayBuffer())
 40 | }
 41 | 
 42 | export function readLocalFile(filePath: string): { success: true; buffer: Buffer } | { success: false; error: string } {
 43 | 	if (!fs.existsSync(filePath)) {
 44 | 		return { success: false, error: `File not found at path: ${filePath}` }
 45 | 	}
 46 | 	
 47 | 	const buffer = fs.readFileSync(filePath)
 48 | 	return { success: true, buffer }
 49 | }
 50 | 
 51 | export async function getFileBuffer(source: string): Promise<{ success: true; buffer: Buffer } | { success: false; error: string }> {
 52 | 	if (isHttpUrl(source)) {
 53 | 		try {
 54 | 			const buffer = await fetchFileFromUrl(source)
 55 | 			return { success: true, buffer }
 56 | 		} catch (error) {
 57 | 			return { success: false, error: `Failed to fetch file from URL: ${error}` }
 58 | 		}
 59 | 	} else {
 60 | 		const filePath = parseFileUri(source)
 61 | 		return readLocalFile(filePath)
 62 | 	}
 63 | }
 64 | 
 65 | export function convertBufferToBlob(buffer: Buffer): Blob {
 66 | 	return new Blob([new Uint8Array(buffer)])
 67 | }
 68 | 
 69 | export function parseExtraFormFields(extraFormJson: string | undefined): Record<string, string> {
 70 | 	if (!extraFormJson) {
 71 | 		return {}
 72 | 	}
 73 | 
 74 | 	try {
 75 | 		const extraForm = JSON.parse(extraFormJson)
 76 | 		const result: Record<string, string> = {}
 77 | 		
 78 | 		for (const [key, value] of Object.entries(extraForm)) {
 79 | 			if (typeof value === "string") {
 80 | 				result[key] = value
 81 | 			} else {
 82 | 				result[key] = JSON.stringify(value)
 83 | 			}
 84 | 		}
 85 | 		
 86 | 		return result
 87 | 	} catch (error) {
 88 | 		console.error("Failed to parse extra form fields:", error)
 89 | 		return {}
 90 | 	}
 91 | }
 92 | 
 93 | export function createErrorResponse(message: string) {
 94 | 	return {
 95 | 		content: [
 96 | 			{
 97 | 				type: "text" as const,
 98 | 				text: message,
 99 | 			},
100 | 		],
101 | 	}
102 | }
103 | 
104 | export function createSuccessResponse(text: string) {
105 | 	return {
106 | 		content: [
107 | 			{
108 | 				type: "text" as const,
109 | 				text,
110 | 			},
111 | 		],
112 | 	}
113 | }
114 | 
115 | export async function convertSvgToPng(svgString: string, width?: number, height?: number): Promise<Buffer> {
116 | 	try {
117 | 		const svgBuffer = Buffer.from(svgString, 'utf8')
118 | 		
119 | 		let sharpInstance = sharp(svgBuffer)
120 | 		
121 | 		if (width || height) {
122 | 			sharpInstance = sharpInstance.resize(width, height, {
123 | 				fit: 'contain',
124 | 				background: { r: 0, g: 0, b: 0, alpha: 0 }
125 | 			})
126 | 		}
127 | 		
128 | 		const pngBuffer = await sharpInstance
129 | 			.png({ 
130 | 				compressionLevel: 6,
131 | 				adaptiveFiltering: true 
132 | 			})
133 | 			.toBuffer()
134 | 		
135 | 		return pngBuffer
136 | 	} catch (error) {
137 | 		throw new Error(`Failed to convert SVG to PNG: ${error}`)
138 | 	}
139 | }
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
  5 | import { FormData, request } from "undici"
  6 | import { z } from "zod"
  7 | import { createRequire } from "node:module"
  8 | import {
  9 | 	validateEnvironmentVariables,
 10 | 	getFileBuffer,
 11 | 	convertBufferToBlob,
 12 | 	parseExtraFormFields,
 13 | 	createErrorResponse,
 14 | 	createSuccessResponse,
 15 | 	convertSvgToPng,
 16 | } from "./utils.js"
 17 | 
 18 | const require = createRequire(import.meta.url)
 19 | const { version: pkgVersion } = require("../package.json") as { version: string }
 20 | 
 21 | const server = new McpServer({
 22 | 	name: "upload-file",
 23 | 	version: pkgVersion,
 24 | })
 25 | 
 26 | async function uploadFileHandler({ source, fileName }: { source: string; fileName: string }) {
 27 | 	const envError = validateEnvironmentVariables()
 28 | 	if (envError) {
 29 | 		return createErrorResponse(envError)
 30 | 	}
 31 | 
 32 | 	const fileResult = await getFileBuffer(source)
 33 | 	if (!fileResult.success) {
 34 | 		return createErrorResponse(fileResult.error)
 35 | 	}
 36 | 
 37 | 	const form = new FormData()
 38 | 	const fileBlob = convertBufferToBlob(fileResult.buffer)
 39 | 	form.append(process.env.FILE_KEY!, fileBlob, fileName)
 40 | 	form.append(process.env.FILE_NAME!, fileName)
 41 | 
 42 | 	const extraFields = parseExtraFormFields(process.env.EXTRA_FORM)
 43 | 	for (const [key, value] of Object.entries(extraFields)) {
 44 | 		form.append(key, value)
 45 | 	}
 46 | 
 47 | 	const uploadResponse = await request(process.env.UPLOAD_URL!, {
 48 | 		method: "POST",
 49 | 		body: form,
 50 | 	})
 51 | 
 52 | 	const text = await uploadResponse.body.text()
 53 | 	return createSuccessResponse(text)
 54 | }
 55 | 
 56 | server.tool(
 57 | 	"upload-file",
 58 | 	"upload file from a url or local file path",
 59 | 	{
 60 | 		source: z.string().describe("url or local file path"),
 61 | 		fileName: z.string().describe("The file name (must be in English)"),
 62 | 	},
 63 | 	uploadFileHandler as any,
 64 | )
 65 | 
 66 | async function uploadSvgHandler({ svgString, fileName, width, height }: { 
 67 | 	svgString: string; 
 68 | 	fileName: string; 
 69 | 	width?: number; 
 70 | 	height?: number; 
 71 | }) {
 72 | 	const envError = validateEnvironmentVariables()
 73 | 	if (envError) {
 74 | 		return createErrorResponse(envError)
 75 | 	}
 76 | 
 77 | 	try {
 78 | 		const pngBuffer = await convertSvgToPng(svgString, width, height)
 79 | 		
 80 | 		const form = new FormData()
 81 | 		const fileBlob = convertBufferToBlob(pngBuffer)
 82 | 		const pngFileName = fileName.replace(/\.svg$/i, '.png')
 83 | 		
 84 | 		form.append(process.env.FILE_KEY!, fileBlob, pngFileName)
 85 | 		form.append(process.env.FILE_NAME!, pngFileName)
 86 | 
 87 | 		const extraFields = parseExtraFormFields(process.env.EXTRA_FORM)
 88 | 		for (const [key, value] of Object.entries(extraFields)) {
 89 | 			form.append(key, value)
 90 | 		}
 91 | 
 92 | 		const uploadResponse = await request(process.env.UPLOAD_URL!, {
 93 | 			method: "POST",
 94 | 			body: form,
 95 | 		})
 96 | 
 97 | 		const text = await uploadResponse.body.text()
 98 | 		return createSuccessResponse(text)
 99 | 	} catch (error) {
100 | 		return createErrorResponse(`Failed to convert SVG to PNG: ${error}`)
101 | 	}
102 | }
103 | 
104 | server.tool(
105 | 	"upload-svg-file",
106 | 	"convert SVG content to PNG image format and upload to server",
107 | 	{
108 | 		svgString: z.string().describe("SVG content as string"),
109 | 		fileName: z.string().describe("The file name (must be in English, .png extension will be added automatically)"),
110 | 		width: z.number().optional().describe("Optional width for PNG output"),
111 | 		height: z.number().optional().describe("Optional height for PNG output"),
112 | 	},
113 | 	uploadSvgHandler as any,
114 | )
115 | 
116 | async function main() {
117 | 	const transport = new StdioServerTransport()
118 | 	await server.connect(transport)
119 | 	console.error("Upload file MCP Server running on stdio")
120 | }
121 | 
122 | // Only run main when not in a test environment
123 | if (!process.env.SKIP_MCP_MAIN) {
124 | 	main().catch((error) => {
125 | 		console.error("Fatal error in main():", error)
126 | 		process.exit(1)
127 | 	})
128 | }
129 | 
130 | export { server, uploadFileHandler, uploadSvgHandler }
131 | 
```

--------------------------------------------------------------------------------
/src/__tests__/index.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, test, beforeEach, beforeAll, afterEach } from "vitest"
  2 | import fs from "node:fs"
  3 | import path from "node:path"
  4 | import { vi } from "vitest"
  5 | // Disable CLI entrypoint when index.ts is imported during tests
  6 | process.env.SKIP_MCP_MAIN = "1"
  7 | 
  8 | // Mock undici at the top level
  9 | vi.mock('undici', async () => {
 10 | 	const actual = await vi.importActual('undici')
 11 | 	
 12 | 	// Mock FormData class
 13 | 	class MockFormData {
 14 | 		private data: Map<string, any> = new Map()
 15 | 		
 16 | 		append(name: string, value: any, filename?: string) {
 17 | 			this.data.set(name, value)
 18 | 		}
 19 | 
 20 | 		get(name: string) {
 21 | 			return this.data.get(name)
 22 | 		}
 23 | 	}
 24 | 	
 25 | 	return {
 26 | 		...actual,
 27 | 		request: vi.fn(),
 28 | 		FormData: MockFormData,
 29 | 	}
 30 | })
 31 | 
 32 | // @ts-ignore -- path resolved by Vitest's TS loader
 33 | let uploadFileHandler: typeof import("../index.js").uploadFileHandler
 34 | let uploadSvgHandler: typeof import("../index.js").uploadSvgHandler
 35 | 
 36 | // Dynamically import the module after setting env vars
 37 | beforeAll(async () => {
 38 | 	// eslint-disable-next-line @typescript-eslint/consistent-type-imports
 39 | 	const mod = await import("../index.js")
 40 | 	uploadFileHandler = mod.uploadFileHandler
 41 | 	uploadSvgHandler = mod.uploadSvgHandler
 42 | })
 43 | 
 44 | // Helper to set required env vars for the handler
 45 | function setDummyEnv() {
 46 | 	process.env.UPLOAD_URL = process.env.UPLOAD_URL || "http://localhost/dummy"
 47 | 	process.env.FILE_KEY = process.env.FILE_KEY || "file"
 48 | 	process.env.FILE_NAME = process.env.FILE_NAME || "fileName"
 49 | }
 50 | 
 51 | describe("uploadFileHandler", () => {
 52 | 	let tempFilePath: string
 53 | 
 54 | 	beforeEach(() => {
 55 | 		setDummyEnv()
 56 | 	})
 57 | 
 58 | 	afterEach(() => {
 59 | 		// Clean up temp files
 60 | 		if (tempFilePath && fs.existsSync(tempFilePath)) {
 61 | 			fs.unlinkSync(tempFilePath)
 62 | 		}
 63 | 	})
 64 | 
 65 | 	test("returns 'File not found' error when given an invalid file URI", async () => {
 66 | 		const invalidUri = "file:///this/path/definitely/does/not/exist.jpeg"
 67 | 		const result = await uploadFileHandler({
 68 | 			source: invalidUri,
 69 | 			fileName: "exist.jpeg",
 70 | 		})
 71 | 
 72 | 		// The handler returns a content array with a single text message
 73 | 		expect(result.content[0]?.text).toMatch(/File not found at path/)
 74 | 	})
 75 | 
 76 | 	test("successfully creates Blob from Buffer for file upload", async () => {
 77 | 		// Create a temporary test file
 78 | 		tempFilePath = path.join(__dirname, "test-image.jpeg")
 79 | 		const testImageBuffer = Buffer.from([
 80 | 			0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01,
 81 | 			0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0xFF, 0xD9
 82 | 		]) // Minimal JPEG header and footer
 83 | 		fs.writeFileSync(tempFilePath, testImageBuffer)
 84 | 
 85 | 		// Mock the undici request
 86 | 		const { request } = await import('undici')
 87 | 		vi.mocked(request).mockResolvedValue({
 88 | 			body: {
 89 | 				text: () => Promise.resolve("Upload successful"),
 90 | 			},
 91 | 		} as any)
 92 | 
 93 | 		const fileUri = `file://${tempFilePath}`
 94 | 		const result = await uploadFileHandler({
 95 | 			source: fileUri,
 96 | 			fileName: "test-image.jpeg",
 97 | 		})
 98 | 
 99 | 		// Verify the upload was attempted
100 | 		expect(request).toHaveBeenCalledWith(
101 | 			process.env.UPLOAD_URL,
102 | 			expect.objectContaining({
103 | 				method: "POST",
104 | 				body: expect.any(Object),
105 | 			})
106 | 		)
107 | 
108 | 		// Verify the result
109 | 		expect(result.content[0]?.text).toBe("Upload successful")
110 | 
111 | 		// Clean up
112 | 		vi.clearAllMocks()
113 | 	})
114 | 
115 | 	test("handles Buffer to Blob conversion for direct file paths", async () => {
116 | 		// Create a temporary test file  
117 | 		tempFilePath = path.join(__dirname, "test-direct.jpeg")
118 | 		const testImageBuffer = Buffer.from([
119 | 			0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01,
120 | 			0x01, 0x01, 0x00, 0x48, 0x00, 0x48, 0x00, 0x00, 0xFF, 0xD9
121 | 		])
122 | 		fs.writeFileSync(tempFilePath, testImageBuffer)
123 | 
124 | 		// Test the core logic: Buffer to Blob conversion
125 | 		const buffer = fs.readFileSync(tempFilePath)
126 | 		const blob = new Blob([buffer])
127 | 		
128 | 		// Verify the conversion works correctly
129 | 		expect(blob).toBeInstanceOf(Blob)
130 | 		expect(blob.size).toBe(buffer.length)
131 | 		expect(blob.type).toBe("") // Default type for unspecified
132 | 
133 | 		// Read the blob back to verify data integrity
134 | 		const arrayBuffer = await blob.arrayBuffer()
135 | 		const resultBuffer = Buffer.from(arrayBuffer)
136 | 		expect(resultBuffer.equals(buffer)).toBe(true)
137 | 	})
138 | 
139 | 	test("converts SVG string to PNG and uploads successfully", async () => {
140 | 		// Simple SVG string for testing
141 | 		const svgString = `<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
142 | 			<rect width="100" height="100" fill="red"/>
143 | 			<text x="50" y="50" text-anchor="middle" fill="white">Test</text>
144 | 		</svg>`
145 | 
146 | 		// Mock the undici request
147 | 		const { request } = await import('undici')
148 | 		vi.mocked(request).mockResolvedValue({
149 | 			body: {
150 | 				text: () => Promise.resolve("SVG upload successful"),
151 | 			},
152 | 		} as any)
153 | 
154 | 		const result = await uploadSvgHandler({
155 | 			svgString,
156 | 			fileName: "test-svg.svg",
157 | 			width: 200,
158 | 			height: 200,
159 | 		})
160 | 
161 | 		// Verify the upload was attempted
162 | 		expect(request).toHaveBeenCalledWith(
163 | 			process.env.UPLOAD_URL,
164 | 			expect.objectContaining({
165 | 				method: "POST",
166 | 				body: expect.any(Object),
167 | 			})
168 | 		)
169 | 
170 | 		// Verify the result
171 | 		expect(result.content[0]?.text).toBe("SVG upload successful")
172 | 
173 | 		// Clean up
174 | 		vi.clearAllMocks()
175 | 	})
176 | 
177 | 	test("handles SVG conversion errors gracefully", async () => {
178 | 		// Invalid SVG string
179 | 		const invalidSvg = "not-valid-svg"
180 | 
181 | 		const result = await uploadSvgHandler({
182 | 			svgString: invalidSvg,
183 | 			fileName: "invalid.svg",
184 | 		})
185 | 
186 | 		// Should return error response
187 | 		expect(result.content[0]?.text).toMatch(/Failed to convert SVG to PNG/)
188 | 	})
189 | 
190 | 	test("generates PNG file from complex SVG for visual verification", async () => {
191 | 		// Complex SVG with gradients and transparency
192 | 		const complexSvgString = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 32 32" class="design-iconfont" width="128" height="128">
193 |   <path fill-rule="evenodd" clip-rule="evenodd" d="M31.9086 6.71102C31.958 6.18599 31.5476 5.73101 31.0204 5.71848C30.3519 5.7026 29.6908 5.67954 29.0384 5.65678C23.9672 5.47989 19.4207 5.32131 16.0003 8.69214C12.5798 5.32131 8.03336 5.47989 2.96217 5.65678L2.96217 5.65678C2.30975 5.67954 1.64864 5.7026 0.980126 5.71848C0.452928 5.73101 0.0425842 6.18599 0.0919159 6.71102L1.45528 21.2213C1.66132 23.4141 3.75793 24.8434 5.93635 24.5188C9.75905 23.9493 12.6329 24.563 16.0003 28.3406C19.3676 24.563 22.2415 23.9493 26.0642 24.5188C28.2426 24.8434 30.3392 23.4141 30.5453 21.2213L31.9086 6.71102ZM16.0979 17.5004V20L17.8264 19.0455C21.9802 16.7519 25.9895 14.538 30 12.3241C29.9869 12.2937 29.9734 12.2636 29.9599 12.2335C29.9464 12.2035 29.9329 12.1734 29.9198 12.143C26.088 13.0035 22.2562 13.8641 18.2786 14.7573V12.0156C15.8748 12.8521 13.6081 13.6405 11.197 14.4784V12C11.0133 12.0612 10.8434 12.1207 10.6823 12.1771C10.3333 12.2994 10.0261 12.407 9.71064 12.4855C9.17387 12.6185 8.99155 12.9045 9.0003 13.4452C9.0211 14.7411 9.01768 16.0377 9.01426 17.3346C9.01265 17.947 9.01103 18.5595 9.01197 19.172C9.01197 19.316 9.02527 19.4599 9.04136 19.6341C9.05026 19.7305 9.06002 19.8361 9.06885 19.9561C9.77616 19.709 10.4742 19.4652 11.1692 19.2225C12.8031 18.6518 14.4196 18.0872 16.0979 17.5004Z" fill="#0DDBFF"></path>
194 |   <path fill-rule="evenodd" clip-rule="evenodd" d="M3.83105 4.89515C3.83105 4.40104 4.23056 3.99771 4.72466 4.00199C9.84936 4.04632 12.4695 5.03339 16 8.5102C19.5305 5.03339 22.1506 4.04632 27.2753 4.00199C27.7694 3.99772 28.1689 4.40104 28.1689 4.89516V12.5362C24.9177 13.2664 21.6506 14.0001 18.2786 14.7573V12.0156C15.8748 12.8521 13.6081 13.6405 11.197 14.4784V12C11.0133 12.0612 10.8434 12.1207 10.6823 12.1771C10.3333 12.2994 10.0261 12.407 9.71064 12.4855C9.17387 12.6185 8.99155 12.9045 9.0003 13.4452C9.0211 14.7411 9.01768 16.0378 9.01426 17.3346C9.01265 17.947 9.01103 18.5595 9.01197 19.172C9.01197 19.316 9.02527 19.4599 9.04136 19.6341C9.05026 19.7305 9.06002 19.8361 9.06885 19.9561C9.77606 19.7091 10.4741 19.4653 11.1689 19.2226C12.8029 18.6519 14.4195 18.0873 16.0979 17.5004V20L17.8195 19.0494C21.3437 17.1034 24.7639 15.2148 28.1689 13.335V19.7889C28.1689 21.9439 26.2717 23.5132 24.1328 23.7757C21.2147 24.1339 19.0168 25.5359 16.0001 28.3411V28.3413L16 28.3412L15.9999 28.3413V28.3411C12.9832 25.5359 10.7853 24.1339 7.86722 23.7757C5.7283 23.5132 3.83105 21.9439 3.83105 19.7889V4.89515Z" fill="#0C5EFF"></path>
195 | </svg>`
196 | 
197 | 		// Test different sizes to verify scaling
198 | 		const sizes = [
199 | 			{ width: 64, height: 64, suffix: "64x64" },
200 | 			{ width: 128, height: 128, suffix: "128x128" },
201 | 			{ width: 256, height: 256, suffix: "256x256" },
202 | 		]
203 | 
204 | 		for (const size of sizes) {
205 | 			// Import the utility function directly to test PNG generation
206 | 			const { convertSvgToPng } = await import("../utils.js")
207 | 			
208 | 			const pngBuffer = await convertSvgToPng(complexSvgString, size.width, size.height)
209 | 			
210 | 			// Save the generated PNG file for visual inspection
211 | 			const outputPath = path.join(__dirname, `test-output-${size.suffix}.png`)
212 | 			fs.writeFileSync(outputPath, pngBuffer)
213 | 			
214 | 			// Verify the buffer is not empty and looks like PNG data
215 | 			expect(pngBuffer.length).toBeGreaterThan(0)
216 | 			// PNG files start with specific magic bytes
217 | 			expect(pngBuffer.subarray(0, 8)).toEqual(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]))
218 | 			
219 | 			console.log(`Generated PNG: ${outputPath} (${pngBuffer.length} bytes, ${size.width}x${size.height})`)
220 | 			
221 | 			// Clean up after test
222 | 			tempFilePath = outputPath
223 | 		}
224 | 	})
225 | })
226 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "compilerOptions": {
  3 |     /* Visit https://aka.ms/tsconfig to read more about this file */
  4 |     /* Projects */
  5 |     // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
  6 |     // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
  7 |     // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
  8 |     // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
  9 |     // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
 10 |     // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */
 11 |     /* Language and Environment */
 12 |     "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
 13 |     // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
 14 |     // "jsx": "preserve",                                /* Specify what JSX code is generated. */
 15 |     // "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
 16 |     // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
 17 |     // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
 18 |     // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
 19 |     // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
 20 |     // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
 21 |     // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
 22 |     // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
 23 |     // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */
 24 |     /* Modules */
 25 |     "module": "Node16", /* Specify what module code is generated. */
 26 |     // "rootDir": "./",                                  /* Specify the root folder within your source files. */
 27 |     "moduleResolution": "Node16", /* Specify how TypeScript looks up a file from a given module specifier. */
 28 |     // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
 29 |     // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
 30 |     // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
 31 |     // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
 32 |     // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
 33 |     // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
 34 |     // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
 35 |     // "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
 36 |     // "rewriteRelativeImportExtensions": true,          /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
 37 |     // "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
 38 |     // "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
 39 |     // "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
 40 |     // "noUncheckedSideEffectImports": true,             /* Check side effect imports. */
 41 |     // "resolveJsonModule": true,                        /* Enable importing .json files. */
 42 |     // "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
 43 |     // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
 44 |     /* JavaScript Support */
 45 |     // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
 46 |     // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
 47 |     // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
 48 |     /* Emit */
 49 |     // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
 50 |     // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
 51 |     // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
 52 |     // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
 53 |     // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
 54 |     // "noEmit": true,                                   /* Disable emitting files from a compilation. */
 55 |     // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
 56 |     // "outDir": "./",                                   /* Specify an output folder for all emitted files. */
 57 |     // "removeComments": true,                           /* Disable emitting comments. */
 58 |     // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
 59 |     // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
 60 |     // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
 61 |     // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
 62 |     // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
 63 |     // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
 64 |     // "newLine": "crlf",                                /* Set the newline character for emitting files. */
 65 |     // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
 66 |     // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
 67 |     // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
 68 |     // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
 69 |     // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
 70 |     /* Interop Constraints */
 71 |     // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
 72 |     // "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
 73 |     // "isolatedDeclarations": true,                     /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
 74 |     // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
 75 |     "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
 76 |     // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
 77 |     "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
 78 |     /* Type Checking */
 79 |     "strict": true, /* Enable all strict type-checking options. */
 80 |     // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
 81 |     // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
 82 |     // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
 83 |     // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
 84 |     // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
 85 |     // "strictBuiltinIteratorReturn": true,              /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
 86 |     // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
 87 |     // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
 88 |     // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
 89 |     // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
 90 |     // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
 91 |     // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
 92 |     // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
 93 |     // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
 94 |     // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
 95 |     // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
 96 |     // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
 97 |     // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
 98 |     // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */
 99 |     /* Completeness */
100 |     // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
101 |     "skipLibCheck": true /* Skip type checking all .d.ts files. */
102 |   },
103 |   "include": [
104 |     "src/**/*"
105 |   ],
106 |   "exclude": [
107 |     "node_modules"
108 |   ]
109 | }
110 | 
```