Open Questions
Design questions that need real use cases to drive decisions.
Bidirectional Streaming
What does a bidirectional streaming method look like?
// 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?
// 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?
// 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:
#[cli(mode = "client", endpoint = "http://localhost:3000")]
impl MyService { ... }
// Generates: `mycli create-user --name foo` → POST /usersCLI Design Principles (from normalize)
Worth adopting as conventions for generated CLIs:
- Group by domain, not verb —
users listnotlist-users - Positional args for primary targets —
mycli get-user <id>notmycli get-user --id <id> listas subcommand, not flag — consistent across all resource types- Filters compose — multiple filters AND together, no special cases
--dry-runon 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(...))]
#[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-teamsubcommands - HTTP:
GET /user/:id,GET /project/:id,GET /team/:id - Schema: each instantiation gets its own
--output-schemareflecting the concrete return type
Implementation Sketch
extract_methods()detects generic methods (non-emptysig.generics)- Parse
#[server(instantiate(...))]for concrete type lists - For each instantiation: syntactic substitution on param types + return type, generate wrapper method with the given
name - Feed synthetic methods into existing protocol pipelines
Open Questions
- Multi-param generics:
fn convert<From, To>()— theT = Foosyntax doesn't scale. MaybeFrom = Json, To = Yaml, name = "json-to-yaml". - Associated types:
config: T::Configrequires the proc macro to resolve associated types through trait bounds — syntactic substitution alone may not suffice. May needconfig_type = FooConfigas 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 explicitname?
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?
#[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 --flagequalsapp status --flag. - Hidden default:
#[cli(default, hidden)]makes the fallback work withoutstatusappearing 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)].
#[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.