Skip to content

feat: ascend-tools SDK, CLI, and MCP server#1

Merged
lostmygithubaccount merged 41 commits intomainfrom
initial-sdk
Feb 27, 2026
Merged

feat: ascend-tools SDK, CLI, and MCP server#1
lostmygithubaccount merged 41 commits intomainfrom
initial-sdk

Conversation

@lostmygithubaccount
Copy link
Copy Markdown
Contributor

@lostmygithubaccount lostmygithubaccount commented Feb 24, 2026

closes https://github.com/ascend-io/product/issues/733

Summary

  • SDK (ascend-tools-core): Rust HTTP client for Instance API /api/v1 — Ed25519 JWT auth with auto-discovery, token caching, typed methods for runtimes, flows, flow runs, pause/resume
  • CLI (ascend-tools-cli): clap commands with table + JSON output, --resume flag on flow run to auto-resume paused runtimes, subcommands show help when called without args
  • MCP server (ascend-tools-mcp): 8 tools over stdio/HTTP for AI assistants (Claude Code, Cursor, etc.) — list/get runtimes, resume/pause, list flows, run flow, list/get flow runs
  • Python bindings (ascend-tools-py): PyO3 Client class exposing all SDK methods, ascend-tools CLI entry point via maturin
  • Skill install (skill install --target <PATH>): installs a SKILL.md teaching AI agents the CLI commands — an alternative to MCP for agent integration. Template is embedded in the binary and written to <target>/ascend-tools/SKILL.md
  • run_flow checks runtime health before submitting; errors on paused/unhealthy runtimes unless --resume/resume=True is passed

Tested end-to-end locally against a booted ASE workspace.

Rust SDK + CLI with Python wrapper for the Ascend REST API.

- Rust SDK crate (ascend-ops): Ed25519 JWT auth, Cloud API token
  exchange, typed HTTP client for Instance API /api/v1 endpoints
- Rust CLI crate (ascend-ops-cli): clap-derived commands for
  runtime/flow/build operations with table + JSON output
- PyO3 binding crate (ascend-ops-py): exposes Client class + CLI run()
- Python wrapper: `from ascend_ops import Client` SDK, `ascend-ops` CLI
- Build scripts mirroring md-cli pattern (bin/build, check, test, etc.)

Refs ascend-io/product#733

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lostmygithubaccount lostmygithubaccount self-assigned this Feb 24, 2026
@lostmygithubaccount
Copy link
Copy Markdown
Contributor Author

Depends on https://github.com/ascend-io/ascend-backend/pull/1240 for the /api/v1/ backend endpoints this SDK calls.

- Drop ASCEND_ORG_ID — use POST /auth/token with instance_api_host instead
- Rename ASCEND_PRIVATE_KEY → ASCEND_SERVICE_ACCOUNT_KEY (old name still works)
- Python Client() reads env vars automatically when no args provided
- Merge flow-run into flow (list-runs, get-run subcommands)
- Add flow list command (lists available flow names)
- Remove build command (not needed for public API)
- Add -r shorthand for --runtime
- Hide secret env var values in --help
- Fix token cache race condition (hold lock during refresh)
- Parse token expiry from response instead of hardcoding 1h
- Table output by default, -o json for machine output
- No subcommand prints help instead of error
- Extract client.py, rename _cli.py → cli.py
- Add CLAUDE.md
- Remove unused clap dep from SDK crate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lostmygithubaccount
Copy link
Copy Markdown
Contributor Author

Updated with all polish: dropped org_id, env var auth, flow list, CLI restructure, token cache fix, CLAUDE.md. Related PRs: https://github.com/ascend-io/ascend-backend/pull/1240, https://github.com/ascend-io/ascend-ui/pull/2190

lostmygithubaccount and others added 16 commits February 24, 2026 18:13
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enables IDE autocomplete and go-to-definition for the ascend_ops package.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ci.yml: runs checks on push/PR to main
- check.yml: reusable workflow (fmt, clippy, test, ruff)
- release.yml: multi-platform Rust binaries on version tags
- release-python.yml: PyPI wheels + sdist on version tags
- bin/setup, bin/build-wheels, bin/build-sdist scripts

Mirrors the md-cli CI/CD pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Token exchange now goes through Instance API's /api/v1/auth/token
instead of calling Cloud API directly. Users only need 3 env vars:
- ASCEND_SERVICE_ACCOUNT_ID
- ASCEND_SERVICE_ACCOUNT_KEY
- ASCEND_INSTANCE_API_URL

Removed ASCEND_CLOUD_API_URL from config, CLI, PyO3, and Python SDK.
ASCEND_CLOUD_API_DOMAIN remains as a local dev override for JWT audience.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SDK now fetches the JWT audience domain from GET /api/v1/auth/config
at first auth, eliminating ASCEND_CLOUD_API_DOMAIN entirely. Users
only need 3 env vars: SA ID, SA key, Instance API URL.

Config is simpler: removed cloud_api_domain and cloud_api_url from
Config struct, CLI flags, and PyO3 binding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove backfill_flow from SDK, CLI, PyO3, Python wrapper, type stubs
- Rename --flow/-f to --flow-name/-f on flow list-runs
- Rename flow= to flow_name= in Python SDK list_flow_runs()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove backfill references, fix flow_name param name, add auth/config
endpoint, remove CLOUD_API_DOMAIN from docstring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename the core Rust SDK crate directory and package from ascend-ops
to ascend-ops-core for clarity alongside ascend-ops-cli and ascend-ops-py.

Updated: Cargo.toml dependencies, build/check/test/format scripts,
CI workflow cache paths, pyproject.toml cache keys, CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dling

- Rename private_key_bytes/private_key_b64 to key_bytes/key_b64 in auth.rs
- Add custom Debug impl for Config to redact service_account_key
- Remove legacy ASCEND_PRIVATE_KEY env var alias
- Fix run_flow spec: use `is not None` so empty dict {} is forwarded
- Update CLAUDE.md conventions for accuracy
- Stop gitignoring Cargo.lock (should be tracked for binaries)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Local-only integration tests that exercise the full API surface against a
running ASE workspace: runtimes (list, get, filter by id/kind), flows
(list), flow runs (list, get, trigger, count verification, pagination,
status filter), and run_flow with empty spec.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename the project wholesale: package names, crate names, CLI binary,
Python module (import ascend_tools), directory structure, CI workflows,
bin scripts, tests, and documentation. GitHub repo already renamed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lostmygithubaccount lostmygithubaccount changed the title feat: initial ascend-ops SDK + CLI feat: initial ascend-tools SDK + CLI Feb 25, 2026
Add ascend-tools-mcp crate with 8 MCP tools (list/get runtimes, resume/pause
runtime, list flows, run flow, list/get flow runs) over stdio and HTTP.

Add runtime lifecycle management: resume_runtime() and pause_runtime() across
SDK, CLI, MCP, and Python bindings. run_flow() now checks runtime health before
submitting and errors if not running; pass --resume (CLI) or resume=True (SDK)
to auto-resume a paused runtime first.

CLI improvements: subcommands show help when called without required args,
HEALTH column shows "paused" for paused runtimes, env var resolution moved
out of clap to fix arg_required_else_help with env vars.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lostmygithubaccount lostmygithubaccount changed the title feat: initial ascend-tools SDK + CLI feat: ascend-tools SDK, CLI, and MCP server Feb 26, 2026
lostmygithubaccount and others added 6 commits February 26, 2026 09:58
The MCP crate referenced rmcp via a local path (../../rust-sdk/) which
doesn't exist in CI. Switch to the published rmcp 0.16 from crates.io.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add coverage for flow run spec fields (full_refresh, parameters,
runner_overrides, multi-field) in both Python and shell tests. Add
pause/resume/--resume cycle to shell tests. Fix timing issues with
health clearing after pause and error capture in shell.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Install a SKILL.md file that teaches AI agents how to use the ascend-tools
CLI. The skill template is embedded in the binary via include_str!() and
written to <target>/ascend-tools/SKILL.md.

Usage: ascend-tools skill install --target ./.claude/skills

No authentication required. Runs before config/client initialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename `wakeup` to `resume` in core SDK for consistency with CLI/Python/MCP
- Share single ureq Agent between Auth and AscendClient (one connection pool)
- Replace glob imports with explicit imports in client, CLI, and MCP server
- Extract `display_health()` helper in CLI to deduplicate health formatting
- Remove duplicated paragraph in CLAUDE.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
lostmygithubaccount and others added 16 commits February 26, 2026 12:37
Move ascend-tools-core, ascend-tools-mcp, and ascend-tools-cli into a
single Cargo workspace at src/ascend_tools/. This gives us one shared
Cargo.lock (down from 3), a single target/ directory, and one-command
workspace operations (cargo test --workspace, cargo clippy --workspace).

The py crate stays outside the workspace (maturin/cdylib constraint)
with its own Cargo.lock.

- Add workspace Cargo.toml with resolver = "3"
- Remove [workspace] stubs from each crate's Cargo.toml
- Delete per-crate Cargo.lock files, replaced by workspace lock
- Simplify bin/ scripts to use workspace commands
- Update CI cache paths to single workspace target
- Update release binary path

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- config.rs: Replace single-element env var arrays with plain &str
  constants; simplify resolve_required signature
- server.rs: Extract `blocking()` helper to deduplicate spawn_blocking
  + error mapping + JSON serialization across all 8 tool handlers
- py lib.rs: Release the GIL during all SDK HTTP calls via py.detach()
  so other Python threads aren't blocked during network I/O

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Release workflows: checkout the correct ref on workflow_dispatch instead
of defaulting to branch HEAD, preventing mismatched binaries for tags.

SDK (run_flow): check runtime state before resuming — only call resume
when the runtime is actually paused, avoiding failures on already-running
runtimes. Omit spec key from request body when None instead of sending
"spec": null. Add skip_serializing_if to FlowRunSpec fields so unset
options aren't serialized as null.

Auth: remove spurious Content-Type: application/json from token exchange
(empty body). Use .expect() instead of .unwrap() on SystemTime.

CLI: add proper clap env bindings with hide_env_values on the SA key.
Expose --since, --until, --offset, --limit on flow list-runs. Require
--http when --bind is passed on the mcp subcommand.

OSS readiness: add MIT LICENSE (Ascend Labs, Inc), remove private PyPI
index from pyproject.toml, add .env to .gitignore, add repository/
homepage URLs to all Cargo.toml and pyproject.toml, add readme to
pyproject.toml, note Windows unsupported in README.

API surface: add #[non_exhaustive] to RuntimeFilters and FlowRunFilters
for semver safety. Add SAFETY comment on unsafe block in reset_sigint.
Update skill.md with missing spec fields.

Apply same fixes (conditional resume, spec omission, Content-Type) to
tests/rest.py reference client.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bump requires-python to >=3.13, update PyO3 ABI target to abi3-py313
- Add forbid(unsafe_code) to core, cli, py crates; deny(unsafe_code) to
  mcp crate with scoped allow on the one necessary libc::signal call
- Add docstrings to all Python type stub methods (core.pyi)
- Document base64 format acceptance on Auth::new (URL-safe and standard)
- Gate reset_sigint() with #[cfg(unix)], add no-op for non-unix
- Add unit tests for auth: PKCS#8 DER encoding (output size, prefix,
  seed embedding, wrong-length rejection, jsonwebtoken roundtrip),
  base64 decoding (URL-safe, standard, invalid, whitespace), JWT signing
  (structure, all 6 claims), Debug redaction
- Add unit tests for config: CLI-overrides-env priority, env fallback,
  empty value handling, error message format; refactor resolve() to be
  a pure function for testability without unsafe env mutation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update requires-python in tests/integration.py and tests/rest.py to
  match the package requirement (>=3.13)
- Remove pytest from dev dependencies (test scripts are standalone uv
  scripts, not pytest modules)
- Remove bin/test-py, simplify bin/test to Rust-only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prepares for adding skill-py.md and skill-mcp.md by giving the CLI
skill template a more specific name. Updates include_str! reference
in cli.rs and paths in CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Auth::new now rejects keys that aren't exactly 32 bytes immediately
after base64 decoding, instead of deferring to JWT signing time.
Also replaces x.com/example.com with ascend.io in test URLs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lostmygithubaccount
Copy link
Copy Markdown
Contributor Author

merging at this point to get the core scaffolding + MVP functionality in place; follow ups for new features & such

@lostmygithubaccount lostmygithubaccount merged commit 34050f4 into main Feb 27, 2026
1 check passed
@lostmygithubaccount lostmygithubaccount deleted the initial-sdk branch February 27, 2026 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant