Skip to content

Commit c766130

Browse files
committed
bootstrap an MCP through Rewatch, with an initial 'diagnose' command
1 parent b7e8c2f commit c766130

File tree

10 files changed

+1348
-56
lines changed

10 files changed

+1348
-56
lines changed

rewatch/Cargo.lock

Lines changed: 951 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rewatch/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ serde = { version = "1.0.152", features = ["derive"] }
2626
serde_json = { version = "1.0.93" }
2727
sysinfo = "0.29.10"
2828
tempfile = "3.10.1"
29+
mcp-server = "0.1.0"
30+
mcp-spec = "0.1.0"
31+
tokio = { version = "1", features = ["rt-multi-thread", "io-std"] }
2932

3033

3134
[profile.release]

rewatch/src/build/compile.rs

Lines changed: 118 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -436,39 +436,17 @@ pub fn compiler_args(
436436
// Command-line --warn-error flag override (takes precedence over rescript.json config)
437437
warn_error_override: Option<String>,
438438
) -> Result<Vec<String>> {
439-
let bsc_flags = config::flatten_flags(&config.compiler_flags);
440-
let dependency_paths = get_dependency_paths(config, project_context, packages, is_type_dev);
441439
let module_name = helpers::file_path_to_module_name(file_path, &config.get_namespace());
442-
443-
let namespace_args = match &config.get_namespace() {
444-
packages::Namespace::NamespaceWithEntry { namespace: _, entry } if &module_name == entry => {
445-
// if the module is the entry we just want to open the namespace
446-
vec![
447-
"-open".to_string(),
448-
config.get_namespace().to_suffix().unwrap().to_string(),
449-
]
450-
}
451-
packages::Namespace::Namespace(_)
452-
| packages::Namespace::NamespaceWithEntry {
453-
namespace: _,
454-
entry: _,
455-
} => {
456-
vec![
457-
"-bs-ns".to_string(),
458-
config.get_namespace().to_suffix().unwrap().to_string(),
459-
]
460-
}
461-
packages::Namespace::NoNamespace => vec![],
462-
};
463-
464-
let root_config = project_context.get_root_config();
465-
let jsx_args = root_config.get_jsx_args();
466-
let jsx_module_args = root_config.get_jsx_module_args();
467-
let jsx_mode_args = root_config.get_jsx_mode_args();
468-
let jsx_preserve_args = root_config.get_jsx_preserve_args();
469-
let gentype_arg = config.get_gentype_arg();
470-
let experimental_args = root_config.get_experimental_features_args();
471-
let warning_args = config.get_warning_args(is_local_dep, warn_error_override);
440+
let base_args = base_compile_args(
441+
config,
442+
file_path,
443+
project_context,
444+
packages,
445+
is_type_dev,
446+
is_local_dep,
447+
warn_error_override,
448+
None,
449+
)?;
472450

473451
let read_cmi_args = match has_interface {
474452
true => {
@@ -483,6 +461,7 @@ pub fn compiler_args(
483461

484462
let package_name_arg = vec!["-bs-package-name".to_string(), config.name.to_owned()];
485463

464+
let root_config = project_context.get_root_config();
486465
let implementation_args = if is_interface {
487466
debug!("Compiling interface file: {}", &module_name);
488467
vec![]
@@ -517,11 +496,116 @@ pub fn compiler_args(
517496
.collect()
518497
};
519498

499+
Ok(vec![
500+
base_args,
501+
read_cmi_args,
502+
// vec!["-warn-error".to_string(), "A".to_string()],
503+
// ^^ this one fails for bisect-ppx
504+
// this is the default
505+
// we should probably parse the right ones from the package config
506+
// vec!["-w".to_string(), "a".to_string()],
507+
package_name_arg,
508+
implementation_args,
509+
vec![ast_path.to_string_lossy().to_string()],
510+
]
511+
.concat())
512+
}
513+
514+
pub fn compiler_args_for_diagnostics(
515+
config: &config::Config,
516+
file_path: &Path,
517+
is_interface: bool,
518+
has_interface: bool,
519+
project_context: &ProjectContext,
520+
packages: &Option<&AHashMap<String, packages::Package>>,
521+
is_type_dev: bool,
522+
is_local_dep: bool,
523+
warn_error_override: Option<String>,
524+
ppx_flags: Vec<String>,
525+
) -> Result<Vec<String>> {
526+
let mut args = base_compile_args(
527+
config,
528+
file_path,
529+
project_context,
530+
packages,
531+
is_type_dev,
532+
is_local_dep,
533+
warn_error_override,
534+
Some(ppx_flags),
535+
)?;
536+
537+
// Gate -bs-read-cmi by .cmi presence to avoid noisy errors when a project hasn't been built yet.
538+
if has_interface && !is_interface {
539+
let pkg_root = config
540+
.path
541+
.parent()
542+
.map(|p| p.to_path_buf())
543+
.unwrap_or_else(|| Path::new(".").to_path_buf());
544+
let ocaml_build_path = packages::get_ocaml_build_path(&pkg_root);
545+
let basename = helpers::file_path_to_compiler_asset_basename(file_path, &config.get_namespace());
546+
let cmi_exists = ocaml_build_path.join(format!("{basename}.cmi")).exists();
547+
if cmi_exists {
548+
args.push("-bs-read-cmi".to_string());
549+
}
550+
}
551+
552+
args.extend([
553+
"-color".to_string(),
554+
"never".to_string(),
555+
"-ignore-parse-errors".to_string(),
556+
"-editor-mode".to_string(),
557+
]);
558+
559+
args.push(file_path.to_string_lossy().to_string());
560+
Ok(args)
561+
}
562+
563+
fn base_compile_args(
564+
config: &config::Config,
565+
file_path: &Path,
566+
project_context: &ProjectContext,
567+
packages: &Option<&AHashMap<String, packages::Package>>,
568+
is_type_dev: bool,
569+
is_local_dep: bool,
570+
warn_error_override: Option<String>,
571+
include_ppx_flags: Option<Vec<String>>,
572+
) -> Result<Vec<String>> {
573+
let bsc_flags = config::flatten_flags(&config.compiler_flags);
574+
let dependency_paths = get_dependency_paths(config, project_context, packages, is_type_dev);
575+
let module_name = helpers::file_path_to_module_name(file_path, &config.get_namespace());
576+
577+
let namespace_args = match &config.get_namespace() {
578+
packages::Namespace::NamespaceWithEntry { namespace: _, entry } if &module_name == entry => {
579+
vec![
580+
"-open".to_string(),
581+
config.get_namespace().to_suffix().unwrap().to_string(),
582+
]
583+
}
584+
packages::Namespace::Namespace(_)
585+
| packages::Namespace::NamespaceWithEntry {
586+
namespace: _,
587+
entry: _,
588+
} => {
589+
vec![
590+
"-bs-ns".to_string(),
591+
config.get_namespace().to_suffix().unwrap().to_string(),
592+
]
593+
}
594+
packages::Namespace::NoNamespace => vec![],
595+
};
596+
597+
let root_config = project_context.get_root_config();
598+
let jsx_args = root_config.get_jsx_args();
599+
let jsx_module_args = root_config.get_jsx_module_args();
600+
let jsx_mode_args = root_config.get_jsx_mode_args();
601+
let jsx_preserve_args = root_config.get_jsx_preserve_args();
602+
let gentype_arg = config.get_gentype_arg();
603+
let experimental_args = root_config.get_experimental_features_args();
604+
let warning_args = config.get_warning_args(is_local_dep, warn_error_override);
520605
let runtime_path_args = get_runtime_path_args(config, project_context)?;
521606

522607
Ok(vec![
523608
namespace_args,
524-
read_cmi_args,
525609
vec![
526610
"-I".to_string(),
527611
Path::new("..").join("ocaml").to_string_lossy().to_string(),
@@ -532,22 +616,11 @@ pub fn compiler_args(
532616
jsx_module_args,
533617
jsx_mode_args,
534618
jsx_preserve_args,
619+
include_ppx_flags.unwrap_or_default(),
535620
bsc_flags.to_owned(),
536621
warning_args,
537622
gentype_arg,
538623
experimental_args,
539-
// vec!["-warn-error".to_string(), "A".to_string()],
540-
// ^^ this one fails for bisect-ppx
541-
// this is the default
542-
// we should probably parse the right ones from the package config
543-
// vec!["-w".to_string(), "a".to_string()],
544-
package_name_arg,
545-
implementation_args,
546-
// vec![
547-
// "-I".to_string(),
548-
// abs_node_modules_path.to_string() + "/rescript/ocaml",
549-
// ],
550-
vec![ast_path.to_string_lossy().to_string()],
551624
]
552625
.concat())
553626
}

rewatch/src/build/parse.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,7 @@ pub fn parser_args(
284284
let root_config = project_context.get_root_config();
285285
let file = &filename;
286286
let ast_path = helpers::get_ast_path(file);
287-
let ppx_flags = config::flatten_ppx_flags(
288-
project_context,
289-
package_config,
290-
&filter_ppx_flags(&package_config.ppx_flags, contents),
291-
)?;
287+
let ppx_flags = ppx_flags_for_contents(project_context, package_config, contents)?;
292288
let jsx_args = root_config.get_jsx_args();
293289
let jsx_module_args = root_config.get_jsx_module_args();
294290
let jsx_mode_args = root_config.get_jsx_mode_args();
@@ -322,6 +318,15 @@ pub fn parser_args(
322318
))
323319
}
324320

321+
pub fn ppx_flags_for_contents(
322+
project_context: &ProjectContext,
323+
package_config: &Config,
324+
contents: &str,
325+
) -> anyhow::Result<Vec<String>> {
326+
let filtered = filter_ppx_flags(&package_config.ppx_flags, contents);
327+
config::flatten_ppx_flags(project_context, package_config, &filtered)
328+
}
329+
325330
fn generate_ast(
326331
package: Package,
327332
filename: &Path,

rewatch/src/cli.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,8 @@ pub enum Command {
425425
Build(BuildArgs),
426426
/// Build, then start a watcher
427427
Watch(WatchArgs),
428+
/// Start a Model Context Protocol (MCP) server over stdio
429+
Mcp {},
428430
/// Clean the build artifacts
429431
Clean {
430432
#[command(flatten)]

rewatch/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod config;
55
pub mod format;
66
pub mod helpers;
77
pub mod lock;
8+
pub mod mcp;
89
pub mod project_context;
910
pub mod queue;
1011
pub mod sourcedirs;

rewatch/src/main.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,23 @@ use console::Term;
33
use log::LevelFilter;
44
use std::{io::Write, path::Path};
55

6-
use rescript::{build, cli, cmd, format, lock, watcher};
6+
use rescript::{build, cli, cmd, format, lock, mcp, watcher};
77

88
fn main() -> Result<()> {
99
let cli = cli::parse_with_default().unwrap_or_else(|err| err.exit());
1010

1111
let log_level_filter = cli.verbose.log_level_filter();
1212

13+
// Route logs to stderr when running the MCP server to keep stdout as a pure JSON-RPC stream.
14+
let logs_to_stdout = !matches!(cli.command, cli::Command::Mcp { .. });
1315
env_logger::Builder::new()
1416
.format(|buf, record| writeln!(buf, "{}:\n{}", record.level(), record.args()))
1517
.filter_level(log_level_filter)
16-
.target(env_logger::fmt::Target::Stdout)
18+
.target(if logs_to_stdout {
19+
env_logger::fmt::Target::Stdout
20+
} else {
21+
env_logger::fmt::Target::Stderr
22+
})
1723
.init();
1824

1925
let mut command = cli.command;
@@ -37,6 +43,13 @@ fn main() -> Result<()> {
3743
println!("{}", build::get_compiler_args(Path::new(&path))?);
3844
std::process::exit(0);
3945
}
46+
cli::Command::Mcp {} => {
47+
if let Err(e) = mcp::run() {
48+
println!("{e}");
49+
std::process::exit(1);
50+
}
51+
std::process::exit(0);
52+
}
4053
cli::Command::Build(build_args) => {
4154
let _lock = get_lock(&build_args.folder);
4255

0 commit comments

Comments
 (0)