Skip to content
Merged
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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI

on:
push:
pull_request:

jobs:
test:
name: Build and Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-

- name: Set up Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true

- name: Run tests
run: cargo test --workspace --all-features --verbose
52 changes: 52 additions & 0 deletions .github/workflows/release-plz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Release-plz

on:
push:
branches:
- main

jobs:
release-plz-release:
name: Release-plz release
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'symposium-dev' }}
permissions:
contents: write
id-token: write
steps:
- &checkout
name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: true
token: ${{ secrets.RELEASE_PLZ_TOKEN }}
- &install-rust
name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: release-plz/action@v0.5
with:
command: release
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}

release-plz-pr:
name: Release-plz PR
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'symposium-dev' }}
permissions:
pull-requests: write
contents: write
concurrency:
group: release-plz-${{ github.ref }}
cancel-in-progress: false
steps:
- *checkout
- *install-rust
- name: Run release-plz
uses: release-plz/action@v0.5
with:
command: release-pr
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}
29 changes: 10 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@ readme = "README.md"
[dependencies]
anyhow = "1.0.100"
indoc = "2.0.7"
pico-args = "0.5.0"
sacp = "10.0.0"
sacp-tokio = "10.0.0"
schemars = "1.1.0"
serde = "1.0.228"
serde_json = "1.0.145"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "process"] }
tokio-util = "0.7.18"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }

[dev-dependencies]
elizacp = "10.0.0"
yopo = "10.0.0"
expect-test = "1.5"
sacp-conductor = "10.0.0"
tempfile = "3.6"
24 changes: 12 additions & 12 deletions src/cargo_mcp.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::cargo_command::execute_cargo_command;
use sacp::{
ProxyToConductor,
JrLink,
mcp_server::{McpContext, McpServer},
};
use schemars::JsonSchema;
Expand Down Expand Up @@ -80,9 +80,9 @@ struct CargoUpdateInputs {
pub extra_args: Option<Vec<String>>,
}

pub fn build_mcp_server(
pub fn build_mcp_server<L: JrLink>(
cwd: Arc<RwLock<Option<String>>>,
) -> McpServer<ProxyToConductor, impl sacp::JrResponder<ProxyToConductor>> {
) -> McpServer<L, impl sacp::JrResponder<L>> {
McpServer::builder("cargo-mcp".to_string())
.instructions(indoc::indoc! {"
Run cargo commands. When possible, always use this instead of calling a shell command. Generally, it makes
Expand All @@ -96,7 +96,7 @@ pub fn build_mcp_server(
"#},
{
let cwd = cwd.clone();
async move |input: SetCwdInputs, _mcp_cx: McpContext<ProxyToConductor>| {
async move |input: SetCwdInputs, _mcp_cx: McpContext<L>| {
*cwd.write().await = input.cwd.clone();

Ok(SetCwdResult {
Expand All @@ -113,7 +113,7 @@ pub fn build_mcp_server(
"#},
{
let cwd = cwd.clone();
async move |input: CargoCommandInputs, _mcp_cx: McpContext<ProxyToConductor>| {
async move |input: CargoCommandInputs, _mcp_cx: McpContext<L>| {
let cwd = if input.cwd.is_some() {
input.cwd
} else {
Expand All @@ -132,7 +132,7 @@ pub fn build_mcp_server(
"#},
{
let cwd = cwd.clone();
async move |input: CargoCommandInputs, _mcp_cx: McpContext<ProxyToConductor>| {
async move |input: CargoCommandInputs, _mcp_cx: McpContext<L>| {
let cwd = if input.cwd.is_some() {
input.cwd
} else {
Expand All @@ -151,7 +151,7 @@ pub fn build_mcp_server(
"#},
{
let cwd = cwd.clone();
async move |input: CargoTestInputs, _mcp_cx: McpContext<ProxyToConductor>| {
async move |input: CargoTestInputs, _mcp_cx: McpContext<L>| {
let args = if let Some(test_arg) = input.test_arg.as_deref() {
vec![test_arg]
} else {
Expand All @@ -176,7 +176,7 @@ pub fn build_mcp_server(
"#},
{
let cwd = cwd.clone();
async move |input: CargoAddInputs, _mcp_cx: McpContext<ProxyToConductor>| {
async move |input: CargoAddInputs, _mcp_cx: McpContext<L>| {
let mut args: Vec<&str> = Vec::new();
args.push(&input.package);
if let Some(extra) = &input.extra_args {
Expand All @@ -201,7 +201,7 @@ pub fn build_mcp_server(
"#},
{
let cwd = cwd.clone();
async move |input: CargoCleanInputs, _mcp_cx: McpContext<ProxyToConductor>| {
async move |input: CargoCleanInputs, _mcp_cx: McpContext<L>| {
let mut args: Vec<&str> = Vec::new();
if let Some(extra) = &input.extra_args {
args.extend(extra.iter().map(|s| s.as_str()));
Expand All @@ -225,7 +225,7 @@ pub fn build_mcp_server(
"#},
{
let cwd = cwd.clone();
async move |input: CargoRemoveInputs, _mcp_cx: McpContext<ProxyToConductor>| {
async move |input: CargoRemoveInputs, _mcp_cx: McpContext<L>| {
let mut args: Vec<&str> = Vec::new();
args.push(&input.package);
if let Some(extra) = &input.extra_args {
Expand All @@ -250,7 +250,7 @@ pub fn build_mcp_server(
"#},
{
let cwd = cwd.clone();
async move |input: CargoRunInputs, _mcp_cx: McpContext<ProxyToConductor>| {
async move |input: CargoRunInputs, _mcp_cx: McpContext<L>| {
let mut args: Vec<&str> = Vec::new();
if input.release.unwrap_or(false) {
args.push("--release");
Expand All @@ -277,7 +277,7 @@ pub fn build_mcp_server(
"#},
{
let cwd = cwd.clone();
async move |input: CargoUpdateInputs, _mcp_cx: McpContext<ProxyToConductor>| {
async move |input: CargoUpdateInputs, _mcp_cx: McpContext<L>| {
let mut args: Vec<&str> = Vec::new();
if let Some(pkg) = input.package.as_deref() {
args.push("-p");
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ impl Component<ProxyToConductor> for CargoProxy {
async fn serve(self, client: impl Component<ConductorToProxy>) -> Result<(), sacp::Error> {
let cwd = Arc::new(RwLock::new(self.workspace_path));
ProxyToConductor::builder()
.name("cargo-proxy")
.name("cargo-proxy")
.with_mcp_server(build_mcp_server(cwd.clone()))
.on_receive_request_from(
ClientPeer,
Expand Down
52 changes: 38 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,51 @@
use std::sync::Arc;

use anyhow::Result;
use sacp::component::Component;
use symposium_cargo::CargoProxy;
use pico_args::Arguments;
use sacp::{ByteStreams, component::Component};
use symposium_cargo::{CargoProxy, build_mcp_server};
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};

/// Run the proxy as a standalone binary connected to stdio
pub async fn run() -> Result<()> {
// Initialize tracing
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();

tracing::info!("Starting cargo-proxy");

pub async fn run_proxy() -> Result<()> {
CargoProxy::default()
.serve(sacp_tokio::Stdio::new())
.await?;

Ok(())
}

pub async fn run_mcp() -> Result<()> {
let mcp = build_mcp_server(Arc::default());
let stido = ByteStreams::new(
tokio::io::stdout().compat_write(),
tokio::io::stdin().compat(),
);
mcp.serve(stido).await?;

Ok(())
}

#[tokio::main]
async fn main() -> Result<()> {
run().await
// Initialize tracing
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("debug")),
)
.init();

let mut args = Arguments::from_env();
let mcp = args.contains("--mcp");
let proxy = args.contains("--proxy");

match (mcp, proxy) {
(true, false) => run_mcp().await,
(false, true) => run_proxy().await,
_ => {
eprintln!("Usage: symposium-cargo [--mcp | --proxy]");
std::process::exit(1);
}
}
}
13 changes: 3 additions & 10 deletions tests/basic_integration.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use anyhow::Result;
use expect_test::expect;
use sacp_conductor::{Conductor, ProxiesAndAgent};
use std::path::PathBuf;
use symposium_cargo::CargoProxy;
Expand Down Expand Up @@ -43,25 +42,19 @@ async fn run_cargo_tool(tool: &str) -> Result<String> {
#[tokio::test]
async fn test_cargo_check() -> Result<()> {
let response = run_cargo_tool("cargo_check").await?;

expect![[r#"OK: CallToolResult { content: [Annotated { raw: Text(RawTextContent { text: "{\"build_success\":false,\"command\":\"cargo check --message-format json\",\"exit_code\":101,\"messages\":[\"error[E0425]: cannot find value `error` in this scope\\n --> src/main.rs:2:5\\n |\\n2 | error\\n | ^^^^^ not found in this scope\\n\\n\",\"For more information about this error, try `rustc --explain E0425`.\\n\"],\"stderr\":\" Checking test-project v0.1.0 (/home/gh-jackh726/symposium/symposium-cargo/tests/test-project)\\nerror: could not compile `test-project` (bin \\\"test-project\\\") due to 1 previous error\"}", meta: None }), annotations: None }], structured_content: Some(Object {"build_success": Bool(false), "command": String("cargo check --message-format json"), "exit_code": Number(101), "messages": Array [String("error[E0425]: cannot find value `error` in this scope\n --> src/main.rs:2:5\n |\n2 | error\n | ^^^^^ not found in this scope\n\n"), String("For more information about this error, try `rustc --explain E0425`.\n")], "stderr": String(" Checking test-project v0.1.0 (/home/gh-jackh726/symposium/symposium-cargo/tests/test-project)\nerror: could not compile `test-project` (bin \"test-project\") due to 1 previous error")}), is_error: Some(false), meta: None }"#]].assert_eq(&response);

assert!(response.contains("cannot find value `error` in this scope"));
Ok(())
}

#[tokio::test]
async fn test_cargo_build() -> Result<()> {
let response = run_cargo_tool("cargo_build").await?;

expect![[r#"OK: CallToolResult { content: [Annotated { raw: Text(RawTextContent { text: "{\"build_success\":false,\"command\":\"cargo build --message-format json\",\"exit_code\":101,\"messages\":[\"error[E0425]: cannot find value `error` in this scope\\n --> src/main.rs:2:5\\n |\\n2 | error\\n | ^^^^^ not found in this scope\\n\\n\",\"For more information about this error, try `rustc --explain E0425`.\\n\"],\"stderr\":\" Compiling test-project v0.1.0 (/home/gh-jackh726/symposium/symposium-cargo/tests/test-project)\\nerror: could not compile `test-project` (bin \\\"test-project\\\") due to 1 previous error\"}", meta: None }), annotations: None }], structured_content: Some(Object {"build_success": Bool(false), "command": String("cargo build --message-format json"), "exit_code": Number(101), "messages": Array [String("error[E0425]: cannot find value `error` in this scope\n --> src/main.rs:2:5\n |\n2 | error\n | ^^^^^ not found in this scope\n\n"), String("For more information about this error, try `rustc --explain E0425`.\n")], "stderr": String(" Compiling test-project v0.1.0 (/home/gh-jackh726/symposium/symposium-cargo/tests/test-project)\nerror: could not compile `test-project` (bin \"test-project\") due to 1 previous error")}), is_error: Some(false), meta: None }"#]].assert_eq(&response);

assert!(response.contains("cannot find value `error` in this scope"));
Ok(())
}
#[tokio::test]
async fn test_cargo_test() -> Result<()> {
let response = run_cargo_tool("cargo_test").await?;

expect![[r#"OK: CallToolResult { content: [Annotated { raw: Text(RawTextContent { text: "{\"build_success\":false,\"command\":\"cargo test --message-format json\",\"exit_code\":101,\"messages\":[\"error[E0425]: cannot find value `error` in this scope\\n --> src/main.rs:2:5\\n |\\n2 | error\\n | ^^^^^ not found in this scope\\n\\n\",\"For more information about this error, try `rustc --explain E0425`.\\n\"],\"stderr\":\" Compiling test-project v0.1.0 (/home/gh-jackh726/symposium/symposium-cargo/tests/test-project)\\nerror: could not compile `test-project` (bin \\\"test-project\\\" test) due to 1 previous error\"}", meta: None }), annotations: None }], structured_content: Some(Object {"build_success": Bool(false), "command": String("cargo test --message-format json"), "exit_code": Number(101), "messages": Array [String("error[E0425]: cannot find value `error` in this scope\n --> src/main.rs:2:5\n |\n2 | error\n | ^^^^^ not found in this scope\n\n"), String("For more information about this error, try `rustc --explain E0425`.\n")], "stderr": String(" Compiling test-project v0.1.0 (/home/gh-jackh726/symposium/symposium-cargo/tests/test-project)\nerror: could not compile `test-project` (bin \"test-project\" test) due to 1 previous error")}), is_error: Some(false), meta: None }"#]].assert_eq(&response);

assert!(response.contains("cannot find value `error` in this scope"));
Ok(())
}