github-mcp 0.1.3

GitHub v3 REST API MCP server, generated by mcpify.
Documentation
// GitHub v3 REST API MCP server — generated by mcpify. Do not hand-edit.
//
// Terminal Client entry point (`search`/`get`/`call`/`test-connection`/
// `config`/`version` run as direct CLI subcommands) and Harness Server
// entry point (`start`/`http` boot the persistent MCP server loop) in one
// binary — Rust's single `[[bin]]`-per-binary-target model doesn't split
// these into two compiled entry points the way `targets::typescript`'s
// index.ts/cli.ts pair does.

mod cli;

use std::sync::Arc;

#[cfg(feature = "profiling")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;

use clap::{Parser, Subcommand};
use tokio::sync::Mutex;

use github_mcp::auth::auth_manager::AuthManager;
use github_mcp::core::component_registry::ComponentRegistry;
use github_mcp::core::config_manager::load_config;
use github_mcp::core::config_schema::Transport;
use github_mcp::core::health_check_manager::HealthCheckManager;
use github_mcp::core::logger::init_logging;
use github_mcp::core::mcp_server::{McpifyServer, connect_stdio};
use github_mcp::core::otel;
use github_mcp::core::shutdown_handler::{install_shutdown_handlers, on_shutdown};
use github_mcp::data::store::{open_store, resolve_store_path};
use github_mcp::http::server::start_http_server;
use github_mcp::http::types::HttpServerConfig;

#[derive(Parser)]
#[command(name = "github-mcp", about = "GitHub v3 REST API MCP server", version)]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    /// Interactively configure GitHub v3 REST API credentials and connection settings
    Setup,
    /// Semantic search for operations matching a natural-language query
    Search {
        query: String,
        #[arg(short = 'l', long, default_value_t = 5)]
        limit: usize,
    },
    /// Show the schema, path, method, and documentation for one operation
    Get { operation_id: String },
    /// Validate arguments, invoke the live API, and validate the response
    Call {
        operation_id: String,
        #[arg(short = 'a', long, default_value = "{}")]
        args: String,
    },
    /// Start the Harness Server over stdio (for agent hosts)
    Start,
    /// Start the Harness Server over HTTP (for agent hosts)
    Http {
        #[arg(long)]
        host: Option<String>,
        #[arg(long)]
        port: Option<u16>,
        #[arg(long)]
        cors_allow: Option<String>,
    },
    /// Verify the configured API URL and credentials are reachable
    TestConnection,
    /// Print the resolved configuration (secrets redacted)
    Config,
    /// Print the installed version
    Version,
    /// List the API spec versions available in this project
    Versions,
}

/// Bootstrap order (architecture.md's Runtime Target Architecture): config
/// -> logger -> tracing -> registry -> shutdown-handler -> db -> auth ->
/// McpServer -> pick transport -> health checks. Mirrors
/// `targets::typescript`'s `index.ts` `main()`.
async fn run_harness_server() -> anyhow::Result<()> {
    let config = load_config(serde_json::Map::new())?;

    let (otel_layer, otel_provider) = match otel::build_layer("github-mcp") {
        Ok((layer, provider)) => (Some(layer), Some(provider)),
        Err(_) => (None, None),
    };
    init_logging(otel_layer);
    install_shutdown_handlers();
    if let Some(provider) = otel_provider {
        on_shutdown(Box::new(move || {
            let provider = provider.clone();
            Box::pin(async move { otel::shutdown_tracing(provider) })
        }));
    }

    let registry = Arc::new(Mutex::new(ComponentRegistry::new()));
    let mut health_checks = HealthCheckManager::new(registry.clone());

    let db_path = resolve_store_path(&config.api_version)?;
    {
        let db_path = db_path.clone();
        health_checks
            .register(
                "store",
                true,
                Box::new(move || {
                    let db_path = db_path.clone();
                    Box::pin(async move {
                        open_store(&db_path)?;
                        Ok(())
                    })
                }),
            )
            .await;
    }

    let health_checks = Arc::new(health_checks);
    let health_check_handle = health_checks.start();
    on_shutdown(Box::new(move || {
        health_check_handle.abort();
        Box::pin(async move {})
    }));

    let auth_manager = Arc::new(Mutex::new(AuthManager::new(config.auth_method)));

    if config.transport == Transport::Http {
        let http_config = HttpServerConfig {
            host: config.host.clone(),
            port: config.port,
            cors_allow: config.cors_allow.clone(),
        };
        let (header_location, header_name) =
            github_mcp::auth::auth_manager::header_location_for(config.auth_method);
        let factory_config = config.clone();
        start_http_server(
            move || {
                Ok(McpifyServer::new(
                    factory_config.api_version.clone(),
                    factory_config.clone(),
                    auth_manager.clone(),
                ))
            },
            &http_config,
            registry,
            header_location,
            header_name.to_string(),
        )
        .await?;
    } else {
        let api_version = config.api_version.clone();
        connect_stdio(McpifyServer::new(api_version, config, auth_manager)).await?;
    }

    Ok(())
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Must run before anything builds a TLS client (our own reqwest client,
    // or opentelemetry-otlp's HTTP exporter): rustls 0.23 requires a
    // process-wide default crypto provider, and nothing installs one
    // automatically once more than one provider feature is in the
    // dependency tree.
    rustls::crypto::aws_lc_rs::default_provider()
        .install_default()
        .expect("failed to install rustls crypto provider");

    #[cfg(feature = "profiling")]
    let _dhat_profiler = dhat::Profiler::new_heap();

    let cli = Cli::parse();

    let result = match cli.command {
        Command::Setup => cli::setup::run().await,
        Command::Search { query, limit } => cli::search::run(&query, limit).await,
        Command::Get { operation_id } => cli::get::run(&operation_id).await,
        Command::Call { operation_id, args } => cli::call::run(&operation_id, &args).await,
        Command::Start => {
            // SAFETY: single-threaded at this point, before `#[tokio::main]`
            // spawns any concurrent work that would also touch env vars.
            unsafe {
                std::env::set_var("GITHUB_MCP_TRANSPORT", "stdio");
            }
            run_harness_server().await
        }
        Command::Http {
            host,
            port,
            cors_allow,
        } => {
            // SAFETY: single-threaded at this point, before `#[tokio::main]`
            // spawns any concurrent work that would also touch env vars.
            unsafe {
                std::env::set_var("GITHUB_MCP_TRANSPORT", "http");
                if let Some(host) = &host {
                    std::env::set_var("GITHUB_MCP_HOST", host);
                }
                if let Some(port) = port {
                    std::env::set_var("GITHUB_MCP_PORT", port.to_string());
                }
                if let Some(cors_allow) = &cors_allow {
                    std::env::set_var("GITHUB_MCP_CORS_ALLOW", cors_allow);
                }
            }
            run_harness_server().await
        }
        Command::TestConnection => cli::test_connection::run().await,
        Command::Config => cli::config::run(),
        Command::Version => cli::version::run(),
        Command::Versions => cli::versions::run(),
    };

    if let Err(err) = result {
        eprintln!("{err}");
        std::process::exit(1);
    }
    Ok(())
}