systemprompt-api 0.2.0

HTTP API server and gateway for systemprompt.io OS
Documentation
use axum::extract::State;
use axum::response::IntoResponse;
use axum::routing::get;
use axum::{Extension, Router};
use std::sync::Arc;
use systemprompt_database::ServiceRepository;
use systemprompt_models::{ApiError, CollectionResponse, RequestContext};
use systemprompt_runtime::AppContext;

use systemprompt_agent::models::a2a::{AgentExtension, McpServerMetadata};
use systemprompt_agent::services::registry::AgentRegistry;

pub async fn handle_agent_registry(
    Extension(_req_ctx): Extension<RequestContext>,
    State(ctx): State<AppContext>,
) -> impl IntoResponse {
    let registry = match AgentRegistry::new() {
        Ok(r) => Arc::new(r),
        Err(e) => {
            tracing::error!(error = %e, "Failed to load agent registry");
            return ApiError::internal_error(format!("Failed to load agent registry: {e}"))
                .into_response();
        },
    };
    let service_repo = match ServiceRepository::new(ctx.db_pool()) {
        Ok(repo) => repo,
        Err(e) => {
            return ApiError::internal_error(format!("Failed to create service repository: {e}"))
                .into_response();
        },
    };
    let api_external_url = &ctx.config().api_external_url;

    match registry.list_agents().await {
        Ok(agents) => {
            let mut agent_cards = Vec::new();

            for agent_config in agents {
                let runtime_status =
                    match service_repo.get_service_by_name(&agent_config.name).await {
                        Ok(Some(service)) => Some((
                            service.status,
                            Some(agent_config.port),
                            service.pid.map(|p| p as u32),
                        )),
                        Ok(None) => Some(("NotStarted".to_string(), Some(agent_config.port), None)),
                        Err(_) => Some(("Unknown".to_string(), Some(agent_config.port), None)),
                    };

                let mcp_extensions = create_mcp_extensions_from_config(
                    &agent_config.metadata.mcp_servers,
                    api_external_url,
                );

                match registry
                    .to_agent_card(
                        &agent_config.name,
                        api_external_url,
                        mcp_extensions,
                        runtime_status,
                    )
                    .await
                {
                    Ok(card) => {
                        agent_cards.push(card);
                    },
                    Err(e) => {
                        tracing::error!(error = %e, "Failed to convert agent to card");
                    },
                }
            }

            agent_cards.sort_by(|a, b| {
                let a_is_default = a
                    .capabilities
                    .extensions
                    .as_ref()
                    .and_then(|exts| exts.iter().find(|e| e.uri == "systemprompt:service-status"))
                    .and_then(|ext| ext.params.as_ref())
                    .and_then(|p| p.get("default"))
                    .and_then(serde_json::Value::as_bool)
                    .unwrap_or(false);

                let b_is_default = b
                    .capabilities
                    .extensions
                    .as_ref()
                    .and_then(|exts| exts.iter().find(|e| e.uri == "systemprompt:service-status"))
                    .and_then(|ext| ext.params.as_ref())
                    .and_then(|p| p.get("default"))
                    .and_then(serde_json::Value::as_bool)
                    .unwrap_or(false);

                b_is_default.cmp(&a_is_default)
            });

            CollectionResponse::new(agent_cards).into_response()
        },
        Err(e) => {
            tracing::error!(error = %e, "Failed to list agents");
            ApiError::internal_error(format!("Failed to retrieve agent registry: {e}"))
                .into_response()
        },
    }
}

pub fn create_mcp_extensions_from_config(
    server_names: &[String],
    base_url: &str,
) -> Vec<AgentExtension> {
    if server_names.is_empty() {
        return vec![];
    }

    let servers_info: Vec<McpServerMetadata> = server_names
        .iter()
        .map(|name| McpServerMetadata {
            name: name.clone(),
            endpoint: format!("{}/api/v1/mcp/{}/mcp", base_url, name),
            auth: "unknown".to_string(),
            status: "unknown".to_string(),
            version: None,
            tools: None,
        })
        .collect();

    let mcp_protocol_version = "2024-11-05".to_string();

    vec![AgentExtension {
        uri: "systemprompt:mcp-tools".to_string(),
        description: Some("MCP tool execution capabilities with server endpoints".to_string()),
        required: Some(true),
        params: Some(serde_json::json!({
            "supported_protocols": [mcp_protocol_version],
            "servers": servers_info
        })),
    }]
}

pub fn router(ctx: &AppContext) -> Router {
    Router::new()
        .route("/", get(handle_agent_registry))
        .with_state(ctx.clone())
}