1- import { MCP_HASH_PATTERN } from '@tempad-dev/mcp-shared'
1+ import { MCP_HASH_HEX_LENGTH , MCP_HASH_PATTERN } from '@tempad-dev/mcp-shared'
22import { nanoid } from 'nanoid'
33import { createHash } from 'node:crypto'
44import {
@@ -68,6 +68,19 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
6868 }
6969
7070 function handleRequest ( req : IncomingMessage , res : ServerResponse ) : void {
71+ const startedAt = Date . now ( )
72+ res . on ( 'finish' , ( ) => {
73+ log . info (
74+ {
75+ method : req . method ,
76+ url : req . url ,
77+ status : res . statusCode ,
78+ durationMs : Date . now ( ) - startedAt
79+ } ,
80+ 'HTTP asset request completed.'
81+ )
82+ } )
83+
7184 res . setHeader ( 'Access-Control-Allow-Origin' , '*' )
7285 res . setHeader ( 'Access-Control-Allow-Methods' , 'GET, POST, OPTIONS' )
7386 res . setHeader ( 'Access-Control-Allow-Headers' , 'Content-Type, X-Asset-Width, X-Asset-Height' )
@@ -79,16 +92,14 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
7992 }
8093
8194 if ( ! req . url ) {
82- res . writeHead ( 400 )
83- res . end ( 'Missing URL' )
95+ sendError ( res , 400 , 'Missing URL' )
8496 return
8597 }
8698
8799 const url = new URL ( req . url , getBaseUrl ( ) )
88100 const segments = url . pathname . split ( '/' ) . filter ( Boolean )
89101 if ( segments . length !== 2 || segments [ 0 ] !== 'assets' ) {
90- res . writeHead ( 404 )
91- res . end ( 'Not Found' )
102+ sendError ( res , 404 , 'Not Found' )
92103 return
93104 }
94105
@@ -104,15 +115,13 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
104115 return
105116 }
106117
107- res . writeHead ( 405 )
108- res . end ( 'Method Not Allowed' )
118+ sendError ( res , 405 , 'Method Not Allowed' )
109119 }
110120
111121 function handleDownload ( req : IncomingMessage , res : ServerResponse , hash : string ) : void {
112122 const record = store . get ( hash )
113123 if ( ! record ) {
114- res . writeHead ( 404 )
115- res . end ( 'Not Found' )
124+ sendError ( res , 404 , 'Asset Not Found' )
116125 return
117126 }
118127
@@ -123,12 +132,10 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
123132 const err = error as NodeJS . ErrnoException
124133 if ( err . code === 'ENOENT' ) {
125134 store . remove ( hash , { removeFile : false } )
126- res . writeHead ( 404 )
127- res . end ( 'Not Found' )
135+ sendError ( res , 404 , 'Asset Not Found' )
128136 } else {
129137 log . error ( { error, hash } , 'Failed to stat asset file.' )
130- res . writeHead ( 500 )
131- res . end ( 'Internal Server Error' )
138+ sendError ( res , 500 , 'Internal Server Error' )
132139 }
133140 return
134141 }
@@ -143,9 +150,10 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
143150 stream . on ( 'error' , ( error ) => {
144151 log . warn ( { error, hash } , 'Failed to stream asset file.' )
145152 if ( ! res . headersSent ) {
146- res . writeHead ( 500 )
153+ sendError ( res , 500 , 'Internal Server Error' )
154+ } else {
155+ res . end ( )
147156 }
148- res . end ( 'Internal Server Error' )
149157 } )
150158 stream . on ( 'open' , ( ) => {
151159 store . touch ( hash )
@@ -155,8 +163,7 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
155163
156164 function handleUpload ( req : IncomingMessage , res : ServerResponse , hash : string ) : void {
157165 if ( ! MCP_HASH_PATTERN . test ( hash ) ) {
158- res . writeHead ( 400 )
159- res . end ( 'Invalid Hash Format' )
166+ sendError ( res , 400 , 'Invalid Hash Format' )
160167 return
161168 }
162169
@@ -187,8 +194,7 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
187194 store . upsert ( existing )
188195 }
189196 store . touch ( hash )
190- res . writeHead ( 200 )
191- res . end ( 'OK' )
197+ sendOk ( res , 200 , 'Asset Already Exists' )
192198 return
193199 }
194200
@@ -223,25 +229,23 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
223229 if ( err ) {
224230 cleanup ( )
225231 if ( err . message === 'PayloadTooLarge' ) {
226- res . writeHead ( 413 )
227- res . end ( 'Payload Too Large' )
232+ sendError ( res , 413 , 'Payload Too Large' )
228233 } else if ( err . code === 'ERR_STREAM_PREMATURE_CLOSE' ) {
229234 log . warn ( { hash } , 'Upload request closed prematurely.' )
235+ sendError ( res , 400 , 'Upload Incomplete' )
230236 } else {
231237 log . error ( { error : err , hash } , 'Upload pipeline failed.' )
232238 if ( ! res . headersSent ) {
233- res . writeHead ( 500 )
234- res . end ( 'Internal Server Error' )
239+ sendError ( res , 500 , 'Internal Server Error' )
235240 }
236241 }
237242 return
238243 }
239244
240- const computedHash = hasher . digest ( 'hex' )
245+ const computedHash = hasher . digest ( 'hex' ) . slice ( 0 , MCP_HASH_HEX_LENGTH )
241246 if ( computedHash !== hash ) {
242247 cleanup ( )
243- res . writeHead ( 400 )
244- res . end ( 'Hash Mismatch' )
248+ sendError ( res , 400 , 'Hash Mismatch' )
245249 return
246250 }
247251
@@ -250,8 +254,7 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
250254 } catch ( error ) {
251255 log . error ( { error, hash } , 'Failed to rename temp file to asset.' )
252256 cleanup ( )
253- res . writeHead ( 500 )
254- res . end ( 'Internal Server Error' )
257+ sendError ( res , 500 , 'Internal Server Error' )
255258 return
256259 }
257260
@@ -263,11 +266,44 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
263266 metadata
264267 } )
265268 log . info ( { hash, size } , 'Stored uploaded asset via HTTP.' )
266- res . writeHead ( 201 )
267- res . end ( 'Created' )
269+ sendOk ( res , 201 , 'Created' , { hash, size } )
268270 } )
269271 }
270272
273+ function sendError (
274+ res : ServerResponse ,
275+ status : number ,
276+ message : string ,
277+ details ?: Record < string , unknown >
278+ ) : void {
279+ if ( ! res . headersSent ) {
280+ res . writeHead ( status , { 'Content-Type' : 'application/json; charset=utf-8' } )
281+ }
282+ res . end (
283+ JSON . stringify ( {
284+ error : message ,
285+ ...details
286+ } )
287+ )
288+ }
289+
290+ function sendOk (
291+ res : ServerResponse ,
292+ status : number ,
293+ message : string ,
294+ data ?: Record < string , unknown >
295+ ) : void {
296+ if ( ! res . headersSent ) {
297+ res . writeHead ( status , { 'Content-Type' : 'application/json; charset=utf-8' } )
298+ }
299+ res . end (
300+ JSON . stringify ( {
301+ message,
302+ ...data
303+ } )
304+ )
305+ }
306+
271307 return {
272308 start,
273309 stop,
0 commit comments