@@ -380,6 +380,9 @@ pub fn build_router(state: ApiState) -> Router {
380380 . route ( "/api/migrate/preview" , post ( handle_migrate_preview) )
381381 . route ( "/api/migrate/apply" , post ( handle_migrate_apply) )
382382 . route ( "/api/migrate/undo" , post ( handle_migrate_undo) )
383+ // OpenAPI / ChatGPT plugin discovery (no auth required)
384+ . route ( "/openapi.json" , get ( handle_openapi) )
385+ . route ( "/.well-known/ai-plugin.json" , get ( handle_plugin_manifest) )
383386 . layer ( cors)
384387 . with_state ( state)
385388}
@@ -388,6 +391,30 @@ async fn health_check() -> &'static str {
388391 "ok"
389392}
390393
394+ async fn handle_openapi ( State ( state) : State < ApiState > ) -> impl IntoResponse {
395+ let default_url = format ! ( "http://{}:{}" , state. http_config. host, state. http_config. port) ;
396+ let server_url = state
397+ . http_config
398+ . plugin
399+ . public_url
400+ . as_deref ( )
401+ . unwrap_or ( & default_url) ;
402+ let spec = crate :: openapi:: build_openapi_spec ( server_url) ;
403+ Json ( spec)
404+ }
405+
406+ async fn handle_plugin_manifest ( State ( state) : State < ApiState > ) -> impl IntoResponse {
407+ let default_url = format ! ( "http://{}:{}" , state. http_config. host, state. http_config. port) ;
408+ let server_url = state
409+ . http_config
410+ . plugin
411+ . public_url
412+ . as_deref ( )
413+ . unwrap_or ( & default_url) ;
414+ let manifest = crate :: openapi:: build_plugin_manifest ( & state. http_config , server_url) ;
415+ Json ( manifest)
416+ }
417+
391418// ---------------------------------------------------------------------------
392419// Read endpoint handlers
393420// ---------------------------------------------------------------------------
@@ -1273,4 +1300,40 @@ mod tests {
12731300 assert_eq ! ( response. status( ) , StatusCode :: TOO_MANY_REQUESTS ) ;
12741301 assert ! ( response. headers( ) . get( "retry-after" ) . is_some( ) ) ;
12751302 }
1303+
1304+ // -----------------------------------------------------------------------
1305+ // OpenAPI / Plugin manifest tests (no auth required)
1306+ // -----------------------------------------------------------------------
1307+
1308+ #[ tokio:: test]
1309+ async fn test_openapi_no_auth_required ( ) {
1310+ let state = test_api_state ( ) ;
1311+ let app = build_router ( state) ;
1312+ let response = app
1313+ . oneshot (
1314+ axum:: http:: Request :: builder ( )
1315+ . uri ( "/openapi.json" )
1316+ . body ( Body :: empty ( ) )
1317+ . unwrap ( ) ,
1318+ )
1319+ . await
1320+ . unwrap ( ) ;
1321+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
1322+ }
1323+
1324+ #[ tokio:: test]
1325+ async fn test_plugin_manifest_no_auth_required ( ) {
1326+ let state = test_api_state ( ) ;
1327+ let app = build_router ( state) ;
1328+ let response = app
1329+ . oneshot (
1330+ axum:: http:: Request :: builder ( )
1331+ . uri ( "/.well-known/ai-plugin.json" )
1332+ . body ( Body :: empty ( ) )
1333+ . unwrap ( ) ,
1334+ )
1335+ . await
1336+ . unwrap ( ) ;
1337+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
1338+ }
12761339}
0 commit comments