Skip to content
Open
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
7 changes: 7 additions & 0 deletions .github/workflows/test-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ jobs:
with:
save-if: ${{ github.ref_name == 'main' }}

# The bun runtime tests run bun from PATH. They're unix-only, like the runtime's bun support.
- name: Set up bun
if: runner.os == 'Linux'
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.4

- name: Compile tests
run: >
cargo test --all-features --no-run
Expand Down
9 changes: 9 additions & 0 deletions crates/tower-cmd/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ pub enum Error {
#[snafu(display("No [project] section found in pyproject.toml"))]
MissingProjectSection,

#[snafu(display("package.json not found at {}", path))]
PackageJsonNotFound { path: String },

#[snafu(display("Failed to parse package.json: {}", source))]
InvalidPackageJson { source: serde_json::Error },

#[snafu(display("No pyproject.toml or package.json found in {}", dir))]
ProjectFileNotFound { dir: String },

// File I/O errors
#[snafu(display("File I/O error: {}", source))]
IoError { source: std::io::Error },
Expand Down
34 changes: 21 additions & 13 deletions crates/tower-cmd/src/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ struct UpdateTowerfileRequest {
/// Compute size for each run: one of basic.xsmall, basic.small,
/// basic.medium, basic.large. Omit to let Tower apply its default.
run_size: Option<String>,
/// What starts the script: python, bash or bun. Omit to follow the
/// script's extension (.sh runs with bash, anything else with Python).
runtime: Option<String>,
}

#[derive(Debug, Deserialize, JsonSchema)]
Expand Down Expand Up @@ -1023,7 +1026,7 @@ impl TowerService {
}

#[tool(
description = "Update Towerfile config (app name, script, description, source, run_size). Use this instead of editing TOML. Optional: working_directory."
description = "Update Towerfile config (app name, script, description, source, run_size, runtime). Use this instead of editing TOML. Optional: working_directory."
)]
async fn tower_file_update(
&self,
Expand All @@ -1045,6 +1048,9 @@ impl TowerService {
if let Some(run_size) = request.run_size {
tf.app.run_size = Some(run_size);
}
if let Some(runtime) = request.runtime {
tf.app.runtime = Some(runtime);
}
Ok("Towerfile updated".into())
})
}
Expand Down Expand Up @@ -1139,21 +1145,18 @@ impl TowerService {
}

#[tool(
description = "Generate Towerfile from pyproject.toml (first step). Don't add build systems to pyproject.toml. Optional: working_directory."
description = "Generate Towerfile from pyproject.toml, or from package.json for a bun app (first step). Don't add build systems to pyproject.toml. Optional: working_directory."
)]
async fn tower_file_generate(
&self,
Parameters(request): Parameters<GenerateTowerfileRequest>,
) -> Result<CallToolResult, McpError> {
let working_dir = Self::resolve_working_directory(&request.common);
let pyproject_path = working_dir.join("pyproject.toml");
let content = match TowerfileGenerator::from_pyproject(
Some(pyproject_path.to_str().unwrap()),
request.script_path.as_deref(),
) {
Ok(content) => content,
Err(e) => return Self::error_result("Failed to generate Towerfile", e),
};
let content =
match TowerfileGenerator::from_dir(&working_dir, request.script_path.as_deref()) {
Ok(content) => content,
Err(e) => return Self::error_result("Failed to generate Towerfile", e),
};

let towerfile_path = working_dir.join("Towerfile");
match std::fs::write(&towerfile_path, &content) {
Expand Down Expand Up @@ -1191,6 +1194,11 @@ All tools accept optional working_directory parameter to specify which project t
DO NOT add [build-system], [tool.hatchling], [tool.setuptools] or similar
Skip this step if project with pyproject.toml already exists

JAVASCRIPT OR TYPESCRIPT PROJECT (if creating new):
Use 'bun init -y' in project directory to create package.json and index.ts
Skip this step if project with package.json already exists
tower_file_generate then writes a Towerfile with runtime = "bun"

1. TOWERFILE (required for all Tower operations):
tower_file_generate → tower_file_update → tower_file_add/edit/remove_parameter → tower_file_validate
CRITICAL: Always use tower_file_update or tower_file_add/edit/remove_parameter to modify
Expand Down Expand Up @@ -1218,8 +1226,8 @@ All tools accept optional working_directory parameter to specify which project t
tower_secrets_create, tower_secrets_list - store credentials, API keys, database passwords

TYPICAL WORKFLOW:
1. Check if pyproject.toml exists (or create with uv init)
2. tower_file_generate - creates Towerfile from pyproject.toml
1. Check if pyproject.toml or package.json exists (or create with uv init / bun init -y)
2. tower_file_generate - creates Towerfile from pyproject.toml or package.json
3. tower_run_local - test locally (can access tower secrets)
4. tower_apps_create - create app in Tower cloud
5. tower_deploy - deploy code to Tower
Expand Down Expand Up @@ -1331,7 +1339,7 @@ Rules:
- MCP tools are the authoritative Tower interface (not wrappers)
- Use tower_file_update/add/edit/remove_parameter to modify Towerfiles (never edit TOML directly)
- DO NOT add hatchling/setuptools to pyproject.toml - Tower handles deployment
- Tower apps need: pyproject.toml (deps only), Python code, Towerfile
- Tower apps need: pyproject.toml (deps only), Python code, Towerfile; or, for bun apps, package.json, JavaScript or TypeScript code, Towerfile
- Pass run parameters as a JSON object {\"key\": \"value\"}, not as CLI flags
- There is no --param or --params flag

Expand Down
4 changes: 4 additions & 0 deletions crates/tower-cmd/src/skill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ uv init

Creates `pyproject.toml`, `main.py`, `README.md`. Keep `pyproject.toml` minimal — `[project]` metadata and dependencies only. No `[build-system]`, `[tool.hatchling]`, or similar. Skip if a `pyproject.toml` already exists.

For a JavaScript or TypeScript app, run `bun init -y` instead, which creates `package.json` and `index.ts`. Skip it if a `package.json` already exists. Without a `pyproject.toml`, `tower_file_generate` reads `package.json` and sets `runtime = "bun"`.

### 1. Towerfile

```
Expand All @@ -192,6 +194,8 @@ Always use `tower_file_update` or `tower_file_add/edit/remove_parameter` to modi

To give an app more compute, set `run_size` via `tower_file_update`: one of `basic.xsmall`, `basic.small`, `basic.medium` (the default), `basic.large`. It is versioned with the code, and self-hosted runners ignore it.

To run JavaScript or TypeScript, set `runtime` to `bun` via `tower_file_update` and point `script` at a `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs` or `.cjs` file. Dependencies come from `package.json` (and `bun.lock`). Without `runtime`, `.sh` scripts run with bash and everything else with Python.

### 2. Local development (preferred)

```
Expand Down
Loading
Loading