1mod catalog;
3mod input;
4mod page;
5mod read;
6
7use crate::{project::VerifyOptions, runs};
8use input::*;
9use rmcp::{ErrorData, ServerHandler, ServiceExt, model::*, service::RequestContext};
10use serde::{Serialize, de::DeserializeOwned};
11use serde_json::{Value, json};
12use std::time::Duration;
13use tokio_util::sync::CancellationToken;
14
15const MAX_WAIT_SECONDS: u64 = 40;
17const DEFAULT_WAIT_SECONDS: u64 = 30;
18const RUN_LIST_LIMIT: u32 = 20;
20const WAIT_CALL_LIMIT: Duration = Duration::from_secs(44);
22const RESPONSE_BYTES: usize = 63 * 1024;
24const ERROR_CHARS: usize = 2048;
26
27#[derive(Clone)]
28pub struct Server {
29 context: runs::Context,
30}
31impl Server {
32 pub fn new(context: runs::Context) -> Self {
33 Self { context }
34 }
35
36 async fn invoke(&self, name: &str, arguments: Value) -> Result<Value, String> {
37 let context = &self.context;
38 match name {
39 "verify_start" => {
40 let args: Start = parse(arguments)?;
41 let options = VerifyOptions {
42 jobs: args.jobs.map(|limit| limit as usize),
43 modes: runs::RunModes {
44 speculate: args.speculate,
45 defer_human: args.defer_human,
46 },
47 ..Default::default()
48 };
49 encode(runs::start(context, &args.scope.0, &options, args.request_key).await?)
50 }
51 "verify_status" => {
52 let args: Run = parse(arguments)?;
53 encode(runs::status(context, &args.run).await?)
54 }
55 "verify_wait" => {
56 let args: Wait = parse(arguments)?;
57 let seconds = args.seconds.unwrap_or(DEFAULT_WAIT_SECONDS);
58 if seconds > MAX_WAIT_SECONDS {
59 return Err("seconds must be an integer from 0 through 40.".into());
60 }
61 encode(
62 tokio::time::timeout(
63 WAIT_CALL_LIMIT,
64 runs::wait(
65 context,
66 &args.run,
67 Some(Duration::from_secs(seconds)),
68 CancellationToken::new(),
69 ),
70 )
71 .await
72 .map_err(|_| "State unavailable within the MCP observation deadline.")??,
73 )
74 }
75 "verify_result" => {
76 let args: ResultInput = parse(arguments)?;
77 encode(runs::result(context, &args.run, args.cursor).await?)
78 }
79 "verify_cancel" => {
80 let args: Run = parse(arguments)?;
81 encode(runs::cancel(context, &args.run).await?)
82 }
83 name => read::inspect(context, name, arguments).await,
84 }
85 }
86}
87fn parse<T: DeserializeOwned>(value: Value) -> Result<T, String> {
88 serde_json::from_value(value).map_err(|error| format!("Invalid arguments: {error}"))
89}
90fn encode(value: impl Serialize) -> Result<Value, String> {
91 serde_json::to_value(value).map_err(|error| error.to_string())
92}
93impl ServerHandler for Server {
94 fn get_info(&self) -> ServerConfig {
95 ServerConfig::new(ServerCapabilities::builder().enable_tools().build())
96 .with_server_info(Implementation::new("artifactize", env!("CARGO_PKG_VERSION")))
97 .with_instructions("Runs belong to artifactize, never the MCP connection. Retain the Run ID; use verify_wait rather than short polling. Human sign-offs belong to people. Read tools page the exact CLI JSON as UTF-8 text; append entries.text until next_cursor is null.")
98 }
99 async fn list_tools(
100 &self,
101 _: Option<PaginatedRequestParams>,
102 _: RequestContext<rmcp::RoleServer>,
103 ) -> Result<ListToolsResult, ErrorData> {
104 Ok(ListToolsResult {
105 tools: catalog::tools(),
106 ..Default::default()
107 })
108 }
109 fn get_tool(&self, name: &str) -> Option<Tool> {
110 catalog::tools().into_iter().find(|tool| tool.name == name)
111 }
112 async fn call_tool(
113 &self,
114 request: CallToolRequestParams,
115 _: RequestContext<rmcp::RoleServer>,
116 ) -> Result<CallToolResponse, ErrorData> {
117 let arguments = Value::Object(request.arguments.unwrap_or_default());
118 let result = match self.invoke(&request.name, arguments).await {
119 Ok(value) => CallToolResult::structured(value),
120 Err(error) => CallToolResult::structured_error(
121 json!({"code":"operation_failed", "message":error.chars().take(ERROR_CHARS).collect::<String>()}),
122 ),
123 };
124 if serde_json::to_vec(&result)
125 .map_err(|error| ErrorData::internal_error(error.to_string(), None))?
126 .len()
127 > RESPONSE_BYTES
128 {
129 return Ok(CallToolResult::structured_error(json!({
130 "code":"response_too_large", "message":"Response exceeds the detail bound; narrow the inspection."
131 })).into());
132 }
133 Ok(result.into())
134 }
135}
136
137pub async fn serve(context: runs::Context) -> Result<(), String> {
139 match Server::new(context).serve(rmcp::transport::stdio()).await {
140 Ok(service) => {
141 service.waiting().await.map_err(|e| e.to_string())?;
142 Ok(())
143 }
144 Err(rmcp::service::ServerInitializeError::ConnectionClosed(_)) => Ok(()),
146 Err(error) => Err(error.to_string()),
147 }
148}