Skip to content

Commit 87ad0f7

Browse files
committed
bootstrap an MCP through Rewatch, with an initial 'diagnose' command
1 parent 63bc9cf commit 87ad0f7

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

472450
let read_cmi_args = match has_interface {
473451
true => {
@@ -482,6 +460,7 @@ pub fn compiler_args(
482460

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

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

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

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

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)