2026-02-19 15:25:22 +00:00
|
|
|
pub mod agents;
|
2026-02-19 17:58:53 +00:00
|
|
|
pub mod agents_sse;
|
2026-02-16 16:24:21 +00:00
|
|
|
pub mod anthropic;
|
|
|
|
|
pub mod assets;
|
|
|
|
|
pub mod chat;
|
|
|
|
|
pub mod context;
|
|
|
|
|
pub mod health;
|
2026-02-16 16:50:50 +00:00
|
|
|
pub mod io;
|
Accept spike 2: MCP HTTP endpoint for workflow and agent tools
Adds POST /mcp endpoint speaking MCP Streamable HTTP (JSON-RPC 2.0)
with 12 tools for workflow management and agent orchestration.
Supports both JSON and SSE response modes. Includes real-time agent
output streaming over SSE, Content-Type validation, and 15 integration
tests (134 total).
Tools: create_story, validate_stories, list_upcoming, get_story_todos,
record_tests, ensure_acceptance, start_agent, stop_agent, list_agents,
get_agent_config, reload_agent_config, get_agent_output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 19:34:03 +00:00
|
|
|
pub mod mcp;
|
2026-02-16 16:24:21 +00:00
|
|
|
pub mod model;
|
2026-02-20 14:42:41 +00:00
|
|
|
pub mod settings;
|
2026-02-19 12:54:04 +00:00
|
|
|
pub mod workflow;
|
2026-02-16 16:35:25 +00:00
|
|
|
|
2026-02-16 16:24:21 +00:00
|
|
|
pub mod project;
|
|
|
|
|
pub mod ws;
|
|
|
|
|
|
2026-02-19 15:25:22 +00:00
|
|
|
use agents::AgentsApi;
|
2026-02-16 16:35:25 +00:00
|
|
|
use anthropic::AnthropicApi;
|
|
|
|
|
use chat::ChatApi;
|
|
|
|
|
use context::AppContext;
|
2026-02-16 16:50:50 +00:00
|
|
|
use io::IoApi;
|
2026-02-16 16:35:25 +00:00
|
|
|
use model::ModelApi;
|
2026-02-16 16:24:21 +00:00
|
|
|
use poem::EndpointExt;
|
Accept spike 2: MCP HTTP endpoint for workflow and agent tools
Adds POST /mcp endpoint speaking MCP Streamable HTTP (JSON-RPC 2.0)
with 12 tools for workflow management and agent orchestration.
Supports both JSON and SSE response modes. Includes real-time agent
output streaming over SSE, Content-Type validation, and 15 integration
tests (134 total).
Tools: create_story, validate_stories, list_upcoming, get_story_todos,
record_tests, ensure_acceptance, start_agent, stop_agent, list_agents,
get_agent_config, reload_agent_config, get_agent_output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 19:34:03 +00:00
|
|
|
use poem::{Route, get, post};
|
2026-02-16 16:35:25 +00:00
|
|
|
use poem_openapi::OpenApiService;
|
|
|
|
|
use project::ProjectApi;
|
2026-02-20 14:42:41 +00:00
|
|
|
use settings::SettingsApi;
|
2026-02-23 11:39:22 +00:00
|
|
|
use std::path::{Path, PathBuf};
|
2026-02-16 16:35:25 +00:00
|
|
|
use std::sync::Arc;
|
2026-02-16 16:24:21 +00:00
|
|
|
|
2026-02-23 11:39:22 +00:00
|
|
|
const DEFAULT_PORT: u16 = 3001;
|
|
|
|
|
|
|
|
|
|
pub fn parse_port(value: Option<String>) -> u16 {
|
|
|
|
|
value
|
|
|
|
|
.and_then(|v| v.parse::<u16>().ok())
|
|
|
|
|
.unwrap_or(DEFAULT_PORT)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn resolve_port() -> u16 {
|
|
|
|
|
parse_port(std::env::var("STORYKIT_PORT").ok())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn write_port_file(dir: &Path, port: u16) -> Option<PathBuf> {
|
|
|
|
|
let path = dir.join(".story_kit_port");
|
|
|
|
|
std::fs::write(&path, port.to_string()).ok()?;
|
|
|
|
|
Some(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn remove_port_file(path: &Path) {
|
|
|
|
|
let _ = std::fs::remove_file(path);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 16:24:21 +00:00
|
|
|
pub fn build_routes(ctx: AppContext) -> impl poem::Endpoint {
|
|
|
|
|
let ctx_arc = std::sync::Arc::new(ctx);
|
|
|
|
|
|
|
|
|
|
let (api_service, docs_service) = build_openapi_service(ctx_arc.clone());
|
|
|
|
|
|
|
|
|
|
Route::new()
|
|
|
|
|
.nest("/api", api_service)
|
|
|
|
|
.nest("/docs", docs_service.swagger_ui())
|
|
|
|
|
.at("/ws", get(ws::ws_handler))
|
2026-02-19 17:58:53 +00:00
|
|
|
.at(
|
Accept story 34: Per-Project Agent Configuration and Role Definitions
Replace single [agent] config with multi-agent [[agent]] roster system.
Each agent has name, role, model, allowed_tools, max_turns, max_budget_usd,
and system_prompt fields that map to Claude CLI flags at spawn time.
- AgentConfig expanded with structured fields, validated at startup (panics
on duplicate names, empty names, non-positive budgets/turns)
- Backwards-compatible: legacy [agent] format auto-wraps with deprecation warning
- AgentPool uses composite "story_id:agent_name" keys for concurrent agents
- agent_name added to AgentEvent variants, AgentInfo, start/stop/subscribe APIs
- GET /agents/config returns roster, POST /agents/config/reload hot-reloads
- POST /agents/start accepts optional agent_name, /agents/stop requires it
- SSE route updated to /agents/:story_id/:agent_name/stream
- Frontend: roster badges, agent selector dropdown, composite-key state
- Project root initialized to cwd at startup so config endpoints work immediately
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:46:14 +00:00
|
|
|
"/agents/:story_id/:agent_name/stream",
|
2026-02-19 17:58:53 +00:00
|
|
|
get(agents_sse::agent_stream),
|
|
|
|
|
)
|
Accept spike 2: MCP HTTP endpoint for workflow and agent tools
Adds POST /mcp endpoint speaking MCP Streamable HTTP (JSON-RPC 2.0)
with 12 tools for workflow management and agent orchestration.
Supports both JSON and SSE response modes. Includes real-time agent
output streaming over SSE, Content-Type validation, and 15 integration
tests (134 total).
Tools: create_story, validate_stories, list_upcoming, get_story_todos,
record_tests, ensure_acceptance, start_agent, stop_agent, list_agents,
get_agent_config, reload_agent_config, get_agent_output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 19:34:03 +00:00
|
|
|
.at(
|
|
|
|
|
"/mcp",
|
|
|
|
|
post(mcp::mcp_post_handler).get(mcp::mcp_get_handler),
|
|
|
|
|
)
|
2026-02-16 16:24:21 +00:00
|
|
|
.at("/health", get(health::health))
|
|
|
|
|
.at("/assets/*path", get(assets::embedded_asset))
|
|
|
|
|
.at("/", get(assets::embedded_index))
|
|
|
|
|
.at("/*path", get(assets::embedded_file))
|
|
|
|
|
.data(ctx_arc)
|
|
|
|
|
}
|
2026-02-16 16:35:25 +00:00
|
|
|
|
2026-02-19 12:54:04 +00:00
|
|
|
type ApiTuple = (
|
|
|
|
|
ProjectApi,
|
|
|
|
|
ModelApi,
|
|
|
|
|
AnthropicApi,
|
|
|
|
|
IoApi,
|
|
|
|
|
ChatApi,
|
2026-02-19 15:25:22 +00:00
|
|
|
AgentsApi,
|
2026-02-20 14:42:41 +00:00
|
|
|
SettingsApi,
|
2026-02-19 12:54:04 +00:00
|
|
|
);
|
2026-02-16 16:35:25 +00:00
|
|
|
|
|
|
|
|
type ApiService = OpenApiService<ApiTuple, ()>;
|
|
|
|
|
|
2026-02-16 16:50:50 +00:00
|
|
|
/// All HTTP methods are documented by OpenAPI at /docs
|
2026-02-16 16:35:25 +00:00
|
|
|
pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
|
|
|
|
|
let api = (
|
|
|
|
|
ProjectApi { ctx: ctx.clone() },
|
|
|
|
|
ModelApi { ctx: ctx.clone() },
|
|
|
|
|
AnthropicApi::new(ctx.clone()),
|
2026-02-16 16:50:50 +00:00
|
|
|
IoApi { ctx: ctx.clone() },
|
2026-02-16 16:35:25 +00:00
|
|
|
ChatApi { ctx: ctx.clone() },
|
2026-02-19 15:25:22 +00:00
|
|
|
AgentsApi { ctx: ctx.clone() },
|
2026-02-20 14:42:41 +00:00
|
|
|
SettingsApi { ctx: ctx.clone() },
|
2026-02-16 16:35:25 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let api_service =
|
2026-02-19 15:30:23 +00:00
|
|
|
OpenApiService::new(api, "Story Kit API", "1.0").server("http://127.0.0.1:3001/api");
|
2026-02-16 16:35:25 +00:00
|
|
|
|
|
|
|
|
let docs_api = (
|
|
|
|
|
ProjectApi { ctx: ctx.clone() },
|
|
|
|
|
ModelApi { ctx: ctx.clone() },
|
|
|
|
|
AnthropicApi::new(ctx.clone()),
|
2026-02-16 16:50:50 +00:00
|
|
|
IoApi { ctx: ctx.clone() },
|
2026-02-19 12:54:04 +00:00
|
|
|
ChatApi { ctx: ctx.clone() },
|
2026-02-20 14:42:41 +00:00
|
|
|
AgentsApi { ctx: ctx.clone() },
|
|
|
|
|
SettingsApi { ctx },
|
2026-02-16 16:35:25 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let docs_service =
|
2026-02-19 15:30:23 +00:00
|
|
|
OpenApiService::new(docs_api, "Story Kit API", "1.0").server("http://127.0.0.1:3001/api");
|
2026-02-16 16:35:25 +00:00
|
|
|
|
|
|
|
|
(api_service, docs_service)
|
|
|
|
|
}
|
2026-02-23 11:39:22 +00:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_port_defaults_to_3001() {
|
|
|
|
|
assert_eq!(parse_port(None), 3001);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_port_reads_valid_value() {
|
|
|
|
|
assert_eq!(parse_port(Some("4200".to_string())), 4200);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_port_ignores_invalid_value() {
|
|
|
|
|
assert_eq!(parse_port(Some("not_a_number".to_string())), 3001);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn write_and_remove_port_file() {
|
|
|
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
|
|
|
|
|
|
|
|
let path = write_port_file(tmp.path(), 4567).expect("should write port file");
|
|
|
|
|
assert_eq!(std::fs::read_to_string(&path).unwrap(), "4567");
|
|
|
|
|
|
|
|
|
|
remove_port_file(&path);
|
|
|
|
|
assert!(!path.exists());
|
|
|
|
|
}
|
|
|
|
|
}
|