AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
MCP verified Apache-2.0 Self-run

Crimson Crab

mcp-singhpratech-crimson-crab · by singhpratech

A Rust SDK for Anthropic's Claude API that won't panic on you — no unwrap/expect/panic, denied at compile time. Full Claude surface: streaming, tools, thinking, prompt caching, batches.

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add mcp-singhpratech-crimson-crab

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-singhpratech-crimson-crab)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
16d ago

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Crimson Crab? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

🦀 crimson-crab

A Rust SDK for Anthropic's Claude API that can't panic on you.

The full Claude API in idiomatic Rust — streaming, tool use, extended thinking, prompt caching, and batches — built on one hard guarantee: no unwrap, expect, panic!, or todo! anywhere in the library, denied at compile time. A malformed or unexpected response is always an Error you handle, never a panic in your service. Wire-faithful types and forward-compatible enums keep working the day Anthropic ships a new model or block type. Claude-only, and deep on purpose.

[](https://crates.io/crates/crimson-crab) [](https://docs.rs/crimson-crab) [](https://github.com/singhpratech/crimson-crab/actions/workflows/ci.yml) [](#license) [](#minimum-supported-rust-version)

191 tests · zero clippy warnings · a panic-free library (unwrap/expect/panic denied at compile time) · MSRV 1.75 · MIT OR Apache-2.0


Install

cargo add crimson-crab

Streaming and batch results are plain [futures_core::Stream]s. To drive them with .next(), add the StreamExt extension trait:

cargo add futures-util

[futures_core::Stream]: https://docs.rs/futures-core/latest/futures_core/stream/trait.Stream.html

30-second quickstart

```rust,norun use crimsoncrab::modelids::CLAUDEOPUS5; use crimsoncrab::prelude::*;

#[tokio::main] async fn main() -> crimsoncrab::Result { // Reads ANTHROPICAPIKEY from the environment. let client = Client::fromenv()?;

let request = MessagesRequest::builder() .model(CLAUDEOPUS5) .max_tokens(1024) .messages(vec![MessageParam::user("Explain Rust's borrow checker in one line.")]) .build()?;

let message = client.messages().create(&request).await?; println!("{}", message.text()); Ok(()) }


`Client` is `Clone + Send + Sync` and shares one connection pool, so build it once and store it in your `axum` state, your MCP server struct, or a plain field — no `Arc`, no `Mutex`, no manual bounds.

## Why crimson-crab

- **Panic-free by construction — the one guarantee for code in your request path.** `unwrap`, `expect`, `panic!`, and `todo!` are denied at compile time across the whole library and enforced in CI — not a style guideline, a lint gate. However surprising the JSON on the wire, you get an `Error` to handle, never a panic in an async task. Few Claude clients make this promise; this one does, and you can verify it in [`src/lib.rs`](src/lib.rs).
- **Wire-faithful types — your code never breaks on new models.** Types mirror the API field-for-field; there are no renamed concepts or leaky abstractions to relearn, and no adapter layer to lag behind a release.
- **Forward-compatible enums — future models work day one.** Every wire enum (content blocks, stream events, deltas, stop reasons, tool definitions, cache TTLs, thinking configs) carries an `Unknown` catch-all that preserves the raw JSON and re-serializes it unchanged instead of erroring. A response from a model the SDK has never heard of — including future **Fable-** and **Mythos-class** models — deserializes cleanly and round-trips verbatim.
- **A tokio-free public API — runs anywhere.** The public surface exposes `futures_core::Stream`, not runtime-specific types; `tokio` is a dev-dependency only. The same builder code compiles for native **and** `wasm32-unknown-unknown` on default features.
- **Official-SDK-parity retries — production-grade out of the box.** Connection errors, timeouts, `408`/`409`/`429`, and `5xx` are retried with full-jitter exponential backoff (0.5s base, 8s cap) and honor `retry-after` — capped at 60s so a hostile or broken server can't park your retry loop for hours. Streaming requests retry only before the first byte.
- **Streaming that never truncates mid-generation.** The client uses an *idle* read timeout rather than a total-request deadline, so a long-but-actively-flowing SSE response is never cut off just because total elapsed time crossed a limit.
- **Tested against real API fixtures.** Every content block and stream event from the wire reference has a serde round-trip test, and every endpoint has `wiremock` coverage — 191 tests, zero clippy warnings.

## Feature coverage

| Capability | crimson-crab | Generic multi-provider clients |
|---|:---:|:---:|
| Messages (create / count tokens) | ✅ | ✅ |
| Fine-grained SSE streaming + accumulated final `Message` | ✅ | usually text-only |
| Tool use (custom tools + server-tool passthrough) | ✅ | partial |
| Extended thinking (adaptive / budgeted / display) | ✅ | rare |
| Prompt caching (`cache_control`, 5m/1h TTLs) | ✅ | rare |
| Structured output (`output_config` JSON Schema) | ✅ | rare |
| Message Batches (create / poll / cancel / stream results) | ✅ | rare |
| Models endpoint (get / list with pagination) | ✅ | varies |
| Forward-compatible unknown-variant handling | ✅ | varies |
| New beta flags without an SDK release (`betas` + `extra_body`) | ✅ | rare |

Claude-specific features are first-class here because Claude is the only API this crate targets. Building against several model vendors? A multi-provider framework will serve you better — [`rig`](https://crates.io/crates/rig-core) and [`genai`](https://crates.io/crates/genai) are genuinely good. **crimson-crab is for teams who have chosen Claude** and want the whole surface, exactly as Anthropic ships it.

## Streaming

Iterate typed events as they arrive; the stream accumulates a complete `Message` for you in the background.

```rust,no_run
use crimson_crab::prelude::*;
use futures_util::StreamExt;

# async fn run(client: &Client, request: &MessagesRequest) -> crimson_crab::Result {
let mut stream = client.messages().stream(request).await?;
while let Some(event) = stream.next().await {
    if let StreamEvent::ContentBlockDelta {
        delta: ContentDelta::TextDelta { text },
        ..
    } = event?
    {
        print!("{text}");
    }
}
// After draining, the accumulated `Message` is identical in shape to a
// non-streaming response.
if let Some(message) = stream.final_message() {
    println!("\n[stop_reason: {:?}]", message.stop_reason);
}
# Ok(())
# }

Relaying text deltas (SSE bodies, channels, web handlers)

MessageStream is Send + Unpin and crimson_crab::Error is Send + Sync + std::error::Error, so a streaming response drops straight into an axum Sse body (or any channel) with no Box::pin and no wrapper error type. Map the event stream down to plain String deltas with filter_map:

```rust,norun use crimsoncrab::prelude::*; use futures_util::StreamExt;

async fn run(client: &Client, request: &MessagesRequest) -> crimson_crab::Result {

// A Stream> of plain text deltas, ready to // hand to an axum Sse body, a channel, or any consumer. let textdeltas = client .messages() .stream(request) .await? .filtermap(|event| async move { match event { Ok(StreamEvent::ContentBlockDelta { delta: ContentDelta::TextDelta { text }, .. }) => Some(Ok(text)), // A late/in-stream error surfaces as an Err item — forward it as a // final event: error frame instead of dropping the connection. Err(e) => Some(Err(e)), Ok(_) => None, } });

forward(text_deltas).await;

Ok(())

}

async fn forward(_deltas: S)

where

S: futures_util::Stream>,

{

}


## Tool use (manual agentic loop)

`message.into_param()` converts a response `Message` straight into a request `MessageParam` — the two `tool_use` blocks echoed verbatim — so the "append the assistant turn, then a user message of tool results" contract is two lines with no lossy serde round-trip. Parallel tool calls need no special handling: just iterate `message.content`.

```rust,no_run
use crimson_crab::prelude::*;
use crimson_crab::types::ToolResultBlockParam;

# async fn run(client: &Client, mut messages: Vec, tool: Tool) -> crimson_crab::Result {
loop {
    let request = MessagesRequest::builder()
        .model("claude-opus-4-8")
        .max_tokens(1024)
        .messages(messages.clone())
        .tool(tool.clone()) // `.tool(_)` appends any `Into`; `.tools(vec)` replaces
        .build()?;
    let message = client.messages().create(&request).await?;

    if message.stop_reason != Some(StopReason::ToolUse) {
        println!("{}", message.text());
        break;
    }

    // Answer every tool call. `ContentBlock` is in the prelude, so matching
    // `ToolUse` needs no extra import.
    let mut results = Vec::new();
    for block in &message.content {
        if let ContentBlock::ToolUse(call) = block {
            match run_tool(&call.name, &call.input) {
                // Success: the discoverable `ContentBlockParam::tool_result` helper.
                Ok(output) => results.push(ContentBlockParam::tool_result(&call.id, output)),
                // Failure: surface it to the model with `is_error: true`.
                Err(why) => results.push(ContentBlockParam::ToolResult(
                    ToolResultBlockParam::error(&call.id, why),
                )),
            }
        }
    }

    // Echo the assistant turn back verbatim, then one user message of results.
    messages.push(message.into_param());
    messages.push(MessageParam::user(results));
}
# Ok(())
# }
// Your tool dispatch. Note `std::result::Result`: see "Imports & the prelude".
# fn run_tool(_name: &str, _input: &serde_json::Value) -> std::result::Result {
#     Ok("tool output".to_string())
# }

Prompt caching & token budgeting

The simplest caching path: a plain-string system prompt plus a top-level cache_control, which auto-places one breakpoint on the last cacheable block — no per-block wiring.

```rust,norun use crimsoncrab::prelude::*;

async fn run(client: &Client) -> crimson_crab::Result {

let request = MessagesRequest::builder() .model("claude-opus-4-8") .maxtokens(256) .system("A long, reusable system prompt worth caching…") .cachecontrol(CacheControl::ephemeral()) // or CacheControl::ephemeral_with_ttl(CacheTtl::OneHour) .messages(vec![MessageParam::user("Restate rule one.")]) .build()?;

let message = client.messages().create(&request).await?; let usage = &message.usage; println!( "fresh input: {} written to cache: {:?} read from cache: {:?}", usage.inputtokens, usage.cachecreationinputtokens, usage.cachereadinput_tokens, );

Ok(())

}


**Token accounting for cost reports.** The three input buckets are **disjoint**: `input_tokens` counts only the uncached input, while `cache_creation_input_tokens` and `cache_read_input_tokens` are separate. Total input tokens = `input_tokens` + `cache_creation_input_tokens` + `cache_read_input_tokens`. Never add the cache buckets *into* `input_tokens` — that double-bills the cached prefix.

Need the number before you spend on generation? Derive a count request from the same messages request — no rebuilding the prompt twice:

```rust,no_run
# use crimson_crab::prelude::*;
# async fn run(client: &Client, request: &MessagesRequest) -> crimson_crab::Result {
let count = client.messages().count_tokens(&request.as_count_request()).await?;
println!("this request will cost {} input tokens", count.input_tokens);
# Ok(())
# }

For fine-grained control you can attach a breakpoint to an individual block instead: build a TextBlockParam (in crimson_crab::types), set cache_control, and pass it as a system block or message content — see [examples/prompt_caching.rs](examples/prompt_caching.rs).

Message Batches

The whole submit → poll → stream → tally pipeline. BatchRequestItem::from_request turns a MessagesRequest into a batch entry with no hand-rolled JSON; BatchStatus and BatchRequestCounts are typed for progress display; and results() decodes the JSONL stream line-by-line, tolerating blank lines and a missing trailing newline.

```rust,norun use crimsoncrab::api::{BatchRequestItem, BatchResultOutcome, BatchStatus}; use crimsoncrab::prelude::*; use futuresutil::StreamExt;

async fn run(client: &Client, request: &MessagesRequest) -> crimson_crab::Result {

// Submit, each entry tagged with your own custom id. let items = vec![BatchRequestItem::from_request("row-1", request)?]; let batch = client.batches().create(&items).await?;

// Poll until the batch reaches a terminal state. (A built-in poll_until_ended // helper is on the v0.2 roadmap; until then, loop with your runtime's timer and // your own deadline guard.) let batch = loop { let current = client.batches().get(&batch.id).await?; if current.processingstatus == BatchStatus::Ended { break current; } tokio::time::sleep(std::time::Duration::fromsecs(30)).await; };

// Results arrive in any order — key them by custom_id, never by position. let mut results = client.batches().results(&batch.id).await?; while let Some(result) = results.next().await { let result = result?; match result.result { BatchResultOutcome::Succeeded(ok) => { println!("{}: {}", result.customid, ok.message.text()); } BatchResultOutcome::Errored(err) => { // err.error is the raw error envelope (a serde_json::Value). println!("{}: errored: {}", result.customid, err.error); } BatchResultOutcome::Canceled() | BatchResultOutcome::Expired() => {} // BatchResultOutcome is #[non_exhaustive]; the wildcard keeps you // forward-compatible with outcome types added in a future release. _ => {} } }

Ok(())

}


## Imports & the prelude

`use crimson_crab::prelude::*;` is the fastest way to get the common types — `Client`, `MessagesRequest`, `MessageParam`, `StreamEvent`, `ContentDelta`, `ContentBlock`, `ContentBlockParam`, `Tool`, `ToolChoice`, `StopReason`, `CacheControl`, and more.

One thing worth knowing: the prelude also re-exports the crate's `Result` and `Error` type aliases, which **shadow** `std::result::Result` / `std::error::Error` inside a glob import. If you write a two-type-argument `Result` in the same scope — common with `axum` handlers, `thiserror`, or macro-heavy crates like `rmcp` — it resolves to the one-argument alias and fails with a confusing `E0107` ("type alias takes 1 generic argument but 2 were supplied"). Two easy fixes:

- Fully-qualify it: `std::result::Result` (as in the tool-loop example above); or
- Skip the glob and import exactly what you need. Curated paths: `crimson_crab::Client`, request/response types under `crimson_crab::api::*` (e.g. `MessagesRequest`), and the wire types under `crimson_crab::types::*` (e.g. `MessageParam`, `TextBlockParam`, `ToolResultBlockParam`).

## Platform support

The crate compiles for native targets and `wasm32-unknown-unknown` on **default features** — no feature juggling. On wasm, `reqwest` resolves to the browser `fetch` backend and TLS features are ignored, so you do not need to disable `rustls-tls`; `cargo tree -i tokio --target wasm32-unknown-unknown` prints nothing.

Two honest caveats for edge/browser deployments: the retry loop cannot sleep on wasm (there are no threads), so on that target retries fire without backoff and do not observe `retry-after` — the browser applies its own backpressure. Streaming type-checks on wasm but is best-effort and not exercised in a headless browser in CI. For a 429-sensitive edge worker, cap `max_retries` accordingly.

## More examples

Runnable programs live in [`examples/`](examples): `basic`, `streaming`, `tool_use`, `thinking`, `prompt_caching`, and `structured_output`. There's also `live_smoke` — a manual end-to-end check that makes real API calls (to

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [singhpratech](https://github.com/singhpratech)
- **Source:** [singhpratech/crimson-crab](https://github.com/singhpratech/crimson-crab)
- **License:** Apache-2.0
- **Homepage:** https://singhpratech.github.io/crimson-crab/

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.