Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,232 changes: 1,213 additions & 19 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ version = "0.1.0"
edition = "2024"

[dependencies]
axum = { version = "0.8", default-features = false, features = ["http1", "tokio"], optional = true }
clap = { version = "4.6.7", features = ["derive"], optional = true }
http = { version = "1", optional = true }
rayon = "1.12.0"
reqwest = { version = "0.13.5", optional = true }
rmcp = { version = "3.5.1", default-features = false, features = ["server", "transport-io", "transport-streamable-http-server"], optional = true }
serde = { version = "1.0.229", features = ["derive"] }
serde_json = { version = "1.0.151", optional = true }
tokio = { version = "1.53.2", features = ["macros", "net", "rt-multi-thread", "signal"], optional = true }
wasmtime = { version = "49.0.2", default-features = false, features = ["runtime", "cranelift", "component-model", "parallel-compilation", "std"], optional = true }
wasmtime-wasi = { version = "49.0.2", default-features = false, features = ["p2"], optional = true }

Expand All @@ -29,6 +34,9 @@ default = ["cli"]
cli = ["dep:clap", "dep:serde_json"]
# `forge::wasm`, which runs plugins compiled to WebAssembly components.
wasm = ["dep:wasmtime", "dep:wasmtime-wasi"]
# `forge mcp`, which serves the methods of a document as the tools of an MCP
# server, over HTTP or standard input and output.
mcp = ["cli", "dep:axum", "dep:http", "dep:reqwest", "dep:rmcp", "dep:tokio"]

[profile.release]
strip = true # Drop symbols and debug info from the binary, which the npm packages ship.
Expand All @@ -45,6 +53,10 @@ required-features = ["cli"]
name = "cli"
required-features = ["cli"]

[[test]]
name = "mcp"
required-features = ["cli", "mcp"]

[[test]]
name = "wasm"
required-features = ["cli", "wasm"]
Expand Down
53 changes: 41 additions & 12 deletions src/bin/forge/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
//! ```text
//! forge openapi.json --target ts --out sdk
//! forge openapi.json --target wasm://plugin.wasm --out sdk
//! forge mcp openapi.json
//! ```
//!
//! `forge mcp`, which serves the methods of the document as the tools of an
//! MCP server, needs the `mcp` feature.

use std::fs;
use std::io::{self, Read};
Expand All @@ -13,21 +17,38 @@ use std::process::ExitCode;
use clap::Parser;
use forge::{Err, Forge};

#[cfg(feature = "mcp")]
mod mcp;
mod ts;

// The document, --target and --out are required unless a command is given,
// and are `Option`s so that a command can leave them out.
/// Generates an SDK from an OpenAPI document.
#[derive(Parser)]
#[command(version)]
#[command(version, args_conflicts_with_subcommands = true, subcommand_negates_reqs = true)]
struct Args {
#[cfg(feature = "mcp")]
#[command(subcommand)]
command: Option<Command>,
/// The OpenAPI JSON document, or - to read it from standard input.
spec: PathBuf,
#[arg(required = true)]
spec: Option<PathBuf>,
/// The language of the SDK: `ts` for TypeScript, or `wasm://PATH` for
/// the plugin compiled to a WebAssembly component at PATH.
#[arg(long, value_parser = Target::parse)]
target: Target,
#[arg(long, value_parser = Target::parse, required = true)]
target: Option<Target>,
/// The directory to write the SDK to.
#[arg(long)]
out: PathBuf,
#[arg(long, required = true)]
out: Option<PathBuf>,
}

/// What forge can do besides generating an SDK.
#[cfg(feature = "mcp")]
#[derive(clap::Subcommand)]
enum Command {
/// Serves the methods of an OpenAPI document as the tools of an MCP
/// server.
Mcp(mcp::Args),
}

/// What forge generates an SDK with.
Expand Down Expand Up @@ -68,20 +89,28 @@ fn main() -> ExitCode {
}
}

/// Generates the SDK that `args` ask for.
/// Runs the command `args` give, or else generates the SDK they ask for.
fn run(args: Args) -> Result<(), Err> {
match args.target {
#[cfg(feature = "mcp")]
if let Some(Command::Mcp(args)) = args.command {
return mcp::run(args, read);
}
// Clap requires these when there is no command.
let (Some(spec), Some(target), Some(out)) = (args.spec, args.target, args.out) else {
unreachable!("clap requires the document, --target and --out");
};
match target {
Target::Ts => {
let forge = read(&args.spec)?;
forge::write(&args.out, forge::run(&forge, &ts::Generator)?)
let forge = read(&spec)?;
forge::write(&out, forge::run(&forge, &ts::Generator)?)
}
#[cfg(feature = "wasm")]
Target::Wasm(path) => {
// Compiling the plugin and reading the document are both slow,
// so they run at the same time.
let (plugin, forge) = rayon::join(|| forge::wasm::WasmPlugin::from_file(&path), || read(&args.spec));
let (plugin, forge) = rayon::join(|| forge::wasm::WasmPlugin::from_file(&path), || read(&spec));
let (plugin, forge) = (plugin?, forge?);
forge::write(&args.out, forge::run(&forge, &plugin)?)
forge::write(&out, forge::run(&forge, &plugin)?)
}
}
}
Expand Down
Loading
Loading