Skip to content

Open Questions ​

Design questions that need real use cases to drive decisions.

Bidirectional Streaming ​

What does a bidirectional streaming method look like?

rust
// Option A: Two streams
fn chat(&self, input: impl Stream<Item=Message>) -> impl Stream<Item=Message>

// Option B: Channel-based
fn chat(&self) -> (Sender<Message>, Receiver<Message>)

// Option C: Callback-based
fn chat(&self, on_message: impl Fn(Message) -> Option<Message>)

Needs: Real use case (chat? live collaboration?) to determine ergonomics.

API Versioning ​

How to handle breaking changes?

rust
// Option A: Attribute-based
#[version("v1")]
fn get_user_v1(&self, id: UserId) -> UserV1

#[version("v2")]
fn get_user(&self, id: UserId) -> User

// Option B: Module-based
mod v1 { impl MyService { ... } }
mod v2 { impl MyService { ... } }

// Option C: Just use different structs
struct MyServiceV1;
struct MyServiceV2;

Needs: Real versioning pain to determine what's worth abstracting.

MCP Beyond Tools ​

MCP has more than tools:

  • Resources - files, data the LLM can read
  • Prompts - pre-built prompt templates
  • Sampling - letting the server request LLM completions

How do these map to Rust?

rust
// Tools are methods (covered)
fn search_users(&self, query: String) -> Vec<User>

// Resources?
#[mcp(resource)]
fn user_data(&self, id: UserId) -> Resource<UserData>

// Prompts?
#[mcp(prompt)]
fn summarize_prompt(&self) -> Prompt {
    prompt!("Summarize the following: {input}")
}

Needs: Real MCP server use case to understand what's valuable.

CLI / Clap Depth ​

The #[cli] macro works for flat subcommands. Several directions to deepen it:

Shell Completions and Man Pages ​

clap_complete and clap_mangen are low-effort, high-value. Could auto-generate a completions subcommand.

CLI as Client Mode ​

When #[http] and #[cli] are on the same service, the CLI could act as a client to the HTTP server:

rust
#[cli(mode = "client", endpoint = "http://localhost:3000")]
impl MyService { ... }
// Generates: `mycli create-user --name foo` → POST /users

CLI Design Principles (from normalize) ​

Worth adopting as conventions for generated CLIs:

  • Group by domain, not verb — users list not list-users
  • Positional args for primary targets — mycli get-user <id> not mycli get-user --id <id>
  • list as subcommand, not flag — consistent across all resource types
  • Filters compose — multiple filters AND together, no special cases
  • --dry-run on every mutating command

Needs: Real CLI use case beyond toy examples to determine priority.

Generic Method Instantiation ​

server-less generates protocol bindings from concrete methods. Generic methods are invisible — there's no way to turn fn get<T: Resource>(&self, id: Id) -> T into CLI subcommands or HTTP routes without knowing the concrete types.

Motivation ​

If server-less is a tool to type arbitrary APIs, this is a gap. Real services have generic methods — CRUD over entity types, format converters, batch processors. Today you'd write N concrete wrapper methods manually, which defeats the point of the derive.

Proposed Design: #[server(instantiate(...))] ​

rust
#[server]
impl MyService {
    #[server(instantiate(
        T = User, name = "user";
        T = Project, name = "project";
        T = Team, name = "team";
    ))]
    pub fn get<T: Resource>(&self, id: Id) -> T {
        self.store.get::<T>(id)
    }
}

The proc macro performs syntactic type substitution on the method signature for each instantiation, generating concrete wrapper methods that feed into the existing protocol pipelines:

  • CLI: get-user, get-project, get-team subcommands
  • HTTP: GET /user/:id, GET /project/:id, GET /team/:id
  • Schema: each instantiation gets its own --output-schema reflecting the concrete return type

Implementation Sketch ​

  1. extract_methods() detects generic methods (non-empty sig.generics)
  2. Parse #[server(instantiate(...))] for concrete type lists
  3. For each instantiation: syntactic substitution on param types + return type, generate wrapper method with the given name
  4. Feed synthetic methods into existing protocol pipelines

Open Questions ​

  • Multi-param generics: fn convert<From, To>() — the T = Foo syntax doesn't scale. Maybe From = Json, To = Yaml, name = "json-to-yaml".
  • Associated types: config: T::Config requires the proc macro to resolve associated types through trait bounds — syntactic substitution alone may not suffice. May need config_type = FooConfig as an explicit param.
  • Where clauses: Complex bounds like where T: Resource + Send + 'static — syntactic substitution handles these, but nested bounds with associated types don't.
  • Naming: Type name lowercased (User → user) as automatic fallback, or always require explicit name?

Status ​

Speculative design. The original use case (normalize's analyze commands with a Metric<E> trait) turned out to not benefit from this — the metric implementations are too heterogeneous to share a generic interface. The general concept is sound for services with genuinely uniform operations across types, but no concrete consumer exists yet.

Needs: A real use case where someone has a generic Rust API and wants server-less to generate typed bindings for each instantiation.

Middleware Ordering ​

When composing multiple extensions, does order matter? How to control it?

rust
#[derive(ServerCore, Auth, RateLimit, Logging, Serve)]
// Is this: Logging(RateLimit(Auth(Core)))?
// Or: Auth(RateLimit(Logging(Core)))?
// Does it matter?

Needs: Real middleware composition to understand ordering requirements.

Testing Generated Code ​

How should users test code that uses server-less derives?

  • Mock the generated server?
  • Test the impl directly?
  • Integration test the whole thing?

Needs: Real testing pain to determine what helpers are useful.


Resolved Questions ​

Default Action on Subcommand Groups ​

Resolved: #[cli(default)] attribute.

#[cli(default)] marks a method as the action to run when no subcommand is given, at any level (root command or subcommand group). The marked method is both a normal subcommand and the fallback — app status and app are equivalent.

  • Flag scoping: flags pass through to the default action via hidden parent-level args; app --flag equals app status --flag.
  • Hidden default: #[cli(default, hidden)] makes the fallback work without status appearing in --help.
  • Peer subcommands: unaffected — other subcommands continue to work normally.

Error Type Unification ​

Resolved: #[derive(ServerlessError)] with inference. See error-mapping.md.

Variant names are inferred to ErrorCode values (e.g., NotFound → 404, InvalidInput → 400). Explicit overrides via #[error(code = NotFound)] or #[error(code = 409)]. Each protocol maps the ErrorCode to its native representation (HTTP status, gRPC code, CLI exit code, JSON-RPC error code).

Auth/Context Injection ​

Resolved: Magic Context parameter. Parameters typed Context are injected from the request context rather than extracted from the API caller's input. The Context type is re-exported from server-less-core.

Nested Command Groups ​

Resolved: Automatic detection via &T return type. See mount-points.md.

Methods returning &T where T: CliSubcommand are automatically detected as subcommand group delegation. No explicit #[cli(subcommand)] attribute needed — opt out with #[cli(skip)] for getters that aren't subcommand groups. The same pattern applies across protocols: HttpMount, McpNamespace, WsMount, JsonRpcMount.

Output Formatting ​

Resolved: Display by default, opt-in JSON. See cli-output-formatting.md.

Default output uses Display (human-readable). Global flags --json, --jsonl, --jq <expr> opt into machine-readable output. jq filtering uses the jaq library in-process (no external binary). --output-schema and --input-schema emit JSON Schema via schemars. display_with escape hatch for custom formatting.

Protocol-Specific Overrides ​

Resolved: Layered attribute system.

  • HTTP: #[route(method = "POST", path = "/custom")] for method/path overrides, #[route(skip)] to exclude
  • Parameters: #[param(query)], #[param(path)], #[param(body)], #[param(header)] for HTTP placement; #[param(short = 'x')], #[param(positional)], #[param(help = "...")] for CLI; #[param(name = "...")] and #[param(default = ...)] cross-protocol
  • CLI: #[cli(skip)], #[cli(display_with = "...")] per-method
  • See param-attributes.md and inference-vs-configuration.md

Richer Type Mapping ​

Resolved: Implemented for common types.

bool → SetTrue flag, Vec<T> → Append (comma-delimited), Option<T> → optional flag. #[param(positional)] for explicit positional args, _id heuristic for automatic positional. #[param(short = 'x')] for short flags.

Cross-Protocol Skip and Hidden ​

Resolved: #[server(skip)] and #[server(hidden)] recognized by every protocol derive.

Repeating #[cli(skip)], #[route(skip)], #[openapi(skip)] etc. for every protocol is tedious. Bare #[skip] is too generic and would clash with other crates' helper attributes, so cross-protocol shorthands live under the server namespace.

Each protocol derive declares server as a recognized helper attribute namespace alongside its own. Since derive macros all receive the full original input simultaneously, there is no ordering or stripping concern — each derive independently checks for #[server(skip)].

rust
#[derive(Cli, Http, OpenApi)]
impl MyService {
    // excluded from all protocols
    #[server(skip)]
    fn internal(&self) -> String { ... }

    // exposed but hidden from help/docs
    #[server(hidden)]
    fn debug_dump(&self) -> String { ... }
}

hidden fans out to each protocol's native concept: omits from CLI completions/help, omits from OpenAPI spec, no-op for HTTP (which has no help system). Per-protocol overrides (#[cli(hidden)], #[route(skip)]) still work for finer control.