1use std::sync::Arc;
13
14use atd_protocol::{Response, ToolVisibility};
15use atd_runtime::capability::CapabilitySet;
16use atd_runtime::dispatch::ServerState;
17use atd_runtime::secrets::BearerIdentity;
18use atd_runtime::tracker::ReadTracker;
19use axum::Json;
20use axum::http::StatusCode;
21use axum::response::IntoResponse;
22use serde::{Deserialize, Serialize};
23use serde_json::{Value, json};
24
25#[derive(Debug, Deserialize)]
29pub struct JsonRpcRequest {
30 #[allow(dead_code)]
31 #[serde(default)]
32 pub jsonrpc: String,
33 #[serde(default)]
36 pub id: Option<Value>,
37 pub method: String,
38 #[serde(default)]
39 pub params: Value,
40}
41
42#[derive(Debug, Serialize)]
45pub struct JsonRpcResponse {
46 pub jsonrpc: &'static str,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub id: Option<Value>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub result: Option<Value>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub error: Option<JsonRpcError>,
53}
54
55#[derive(Debug, Serialize)]
56pub struct JsonRpcError {
57 pub code: i32,
58 pub message: String,
59}
60
61pub fn error_response(
66 status: StatusCode,
67 id: Option<Value>,
68 code: i32,
69 message: impl Into<String>,
70) -> axum::response::Response {
71 error_response_with_headers(status, id, code, message, &[])
72}
73
74pub fn error_response_with_headers(
80 status: StatusCode,
81 id: Option<Value>,
82 code: i32,
83 message: impl Into<String>,
84 extra_headers: &[(&str, String)],
85) -> axum::response::Response {
86 let mut resp: axum::response::Response = (
87 status,
88 Json(JsonRpcResponse {
89 jsonrpc: "2.0",
90 id,
91 result: None,
92 error: Some(JsonRpcError {
93 code,
94 message: message.into(),
95 }),
96 }),
97 )
98 .into_response();
99
100 if !extra_headers.is_empty() {
101 let headers = resp.headers_mut();
102 for (name, value) in extra_headers {
103 if let (Ok(hn), Ok(hv)) = (
104 axum::http::HeaderName::from_bytes(name.as_bytes()),
105 axum::http::HeaderValue::from_str(value),
106 ) {
107 headers.insert(hn, hv);
108 }
109 }
110 }
111
112 resp
113}
114
115pub fn ok_response(id: Option<Value>, result: Value) -> axum::response::Response {
117 Json(JsonRpcResponse {
118 jsonrpc: "2.0",
119 id,
120 result: Some(result),
121 error: None,
122 })
123 .into_response()
124}
125
126pub fn handle_initialize(
135 id: Option<Value>,
136 server_version: &str,
137 token_broker: Option<&Arc<dyn atd_runtime::TokenBroker>>,
138) -> axum::response::Response {
139 let (name, version) = split_server_version(server_version);
140 let mut capabilities = json!({ "tools": {} });
141
142 if let Some(broker) = token_broker {
148 let formats = broker.accepted_token_formats();
149 if !formats.is_empty() {
150 let formats_value = serde_json::Value::Array(
151 formats
152 .iter()
153 .map(|f| serde_json::Value::String((*f).to_string()))
154 .collect(),
155 );
156 capabilities["experimental"] = json!({
157 "atd": {
158 "acceptedTokenFormats": formats_value
159 }
160 });
161 }
162 }
163
164 let info = json!({
165 "protocolVersion": "2025-06-18",
166 "serverInfo": {
167 "name": name,
168 "version": version,
169 },
170 "capabilities": capabilities,
171 });
172 ok_response(id, info)
173}
174
175fn split_server_version(s: &str) -> (String, String) {
176 if let Some(idx) = s.rfind(' ') {
181 let (name, ver) = s.split_at(idx);
182 (name.to_string(), ver.trim().to_string())
183 } else {
184 (s.to_string(), "0.0.0".to_string())
185 }
186}
187
188pub fn handle_initialized_notification(id: Option<Value>) -> axum::response::Response {
191 ok_response(id, json!({}))
192}
193
194pub fn handle_tools_list(id: Option<Value>, state: &ServerState) -> axum::response::Response {
201 let tools: Vec<Value> = state
202 .registry
203 .summaries()
204 .into_iter()
205 .filter(|s| !matches!(s.visibility, ToolVisibility::Hidden))
206 .map(|summary| {
207 let def_extras = state
214 .registry
215 .get(&summary.id)
216 .map(|t| {
217 let def = t.definition();
218 json!({
219 "version": def.version,
220 "outputSchema": def.output_schema,
221 })
222 })
223 .unwrap_or_else(|| json!({}));
224 let mut entry = json!({
225 "name": summary.id,
226 "description": summary.description,
227 "inputSchema": summary.input_schema.clone().unwrap_or_else(|| json!({})),
228 });
229 if let Some(obj) = entry.as_object_mut() {
230 if let Some(extra_obj) = def_extras.as_object() {
231 for (k, v) in extra_obj {
232 obj.insert(k.clone(), v.clone());
233 }
234 }
235 }
236 entry
237 })
238 .collect();
239 ok_response(id, json!({ "tools": tools }))
240}
241
242pub async fn handle_tools_call(
256 id: Option<Value>,
257 state: &Arc<ServerState>,
258 identity: Option<&BearerIdentity>,
259 params: Value,
260) -> axum::response::Response {
261 let tool_id = match params.get("name").and_then(|v| v.as_str()) {
262 Some(s) => s.to_string(),
263 None => {
264 return error_response(StatusCode::OK, id, -32602, "missing `name` parameter");
265 }
266 };
267 let mut args = params
268 .get("arguments")
269 .cloned()
270 .unwrap_or_else(|| Value::Object(Default::default()));
271
272 let cursor: Option<String> = match args.as_object_mut() {
276 Some(obj) => obj
277 .remove("__cursor")
278 .and_then(|v| v.as_str().map(String::from)),
279 None => None,
280 };
281
282 let caps = Arc::new(build_caps(state, identity));
286 let caller_id = identity.map(|i| i.caller_id.clone());
287 let tracker = Arc::new(ReadTracker::new());
291
292 let resp = if let Some(c) = cursor {
293 atd_runtime::dispatch::run_tool_continue(
294 state,
295 &tracker,
296 &caps,
297 caller_id.as_deref(),
298 tool_id.clone(),
299 c,
300 )
301 .await
302 } else {
303 atd_runtime::dispatch::run_tool(
304 state,
305 &tracker,
306 &caps,
307 caller_id.as_deref(),
308 tool_id.clone(),
309 args,
310 false, )
312 .await
313 };
314
315 wrap_tool_response(id, resp)
316}
317
318fn build_caps(state: &ServerState, identity: Option<&BearerIdentity>) -> CapabilitySet {
324 let Some(id) = identity else {
325 return CapabilitySet::empty();
326 };
327 let allow = CapabilitySet::from_iter(state.config.granted_capabilities.iter().cloned());
328 let (granted, _denied) = allow.intersect(&id.granted_capabilities);
329 CapabilitySet::from_iter(granted)
330}
331
332pub fn wrap_tool_response(id: Option<Value>, resp: Response) -> axum::response::Response {
338 match resp {
339 Response::ToolResultResponse {
340 result,
341 success,
342 dry_run: _,
343 tool_id: _,
344 next_cursor,
345 } => {
346 let text = serde_json::to_string(&result).unwrap_or_else(|_| "{}".into());
347 let mut body = json!({
354 "content": [{ "type": "text", "text": text }],
355 "isError": !success,
356 });
357 if let Some(cur) = next_cursor {
358 if let Some(obj) = body.as_object_mut() {
359 obj.insert("nextCursor".into(), Value::String(cur));
360 }
361 }
362 ok_response(id, body)
363 }
364 Response::Error {
365 message,
366 code,
367 retryable,
368 details,
369 } => {
370 let jsonrpc_code = -32603i32;
377 let mut data = serde_json::Map::new();
378 if let Some(c) = code {
379 data.insert("atd_code".into(), Value::from(c));
380 }
381 if let Some(r) = retryable {
382 data.insert("retryable".into(), Value::Bool(r));
383 }
384 if let Some(d) = details {
385 data.insert("details".into(), d);
386 }
387 let err = JsonRpcError {
388 code: jsonrpc_code,
389 message,
390 };
391 let envelope = JsonRpcResponse {
395 jsonrpc: "2.0",
396 id,
397 result: None,
398 error: Some(err),
399 };
400 let mut value = serde_json::to_value(&envelope).unwrap_or_else(|_| {
401 json!({"jsonrpc":"2.0", "error": {"code": jsonrpc_code, "message": "serialise"}})
402 });
403 if !data.is_empty() {
404 if let Some(err_obj) = value.get_mut("error").and_then(|e| e.as_object_mut()) {
405 err_obj.insert("data".into(), Value::Object(data));
406 }
407 }
408 (StatusCode::OK, Json(value)).into_response()
409 }
410 other => error_response(
413 StatusCode::INTERNAL_SERVER_ERROR,
414 id,
415 -32603,
416 format!("unexpected dispatch variant: {other:?}"),
417 ),
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424 use atd_runtime::dispatch::SharedServerConfig;
425 use atd_runtime::registry::{CallFuture, Registry, Tool};
426 use atd_runtime::{CallContext, TierPolicy};
427 use http_body_util::BodyExt;
428 use std::sync::Arc;
429
430 fn stub_def(id: &str) -> atd_protocol::ToolDefinition {
431 use atd_protocol::{
432 BindingProtocol, SafetyLevel, ToolBinding, ToolCapability, ToolResources, ToolSafety,
433 ToolTier, ToolTrust, ToolVisibility, TrustLevel,
434 };
435 let _ = ToolTier::Warm; atd_protocol::ToolDefinition {
437 id: id.into(),
438 name: id.into(),
439 description: "stub".into(),
440 version: "0.0.0".into(),
441 capability: ToolCapability {
442 domain: "echo".into(),
443 actions: vec![],
444 tags: vec![],
445 intent_examples: vec![],
446 },
447 input_schema: json!({"type":"object"}),
448 output_schema: json!({"type":"object"}),
449 bindings: vec![ToolBinding {
450 protocol: BindingProtocol::Cli,
451 config: json!({}),
452 }],
453 safety: ToolSafety {
454 level: SafetyLevel::Read,
455 dry_run: false,
456 side_effects: vec![],
457 data_sensitivity: None,
458 },
459 resources: ToolResources {
460 timeout_ms: 1000,
461 max_concurrent: 1,
462 rate_limit_per_min: None,
463 estimated_tokens: None,
464 },
465 trust: ToolTrust {
466 publisher: "test".into(),
467 trust_level: TrustLevel::L0Unverified,
468 signature: None,
469 },
470 visibility: ToolVisibility::Read,
471 required_capabilities: vec![],
472 tier: None,
473 errors: vec![],
474 }
475 }
476
477 struct EchoStub {
478 def: atd_protocol::ToolDefinition,
479 }
480 impl EchoStub {
481 fn new() -> Self {
482 Self {
483 def: stub_def("ref:echo.say"),
484 }
485 }
486 }
487 impl Tool for EchoStub {
488 fn definition(&self) -> &atd_protocol::ToolDefinition {
489 &self.def
490 }
491 fn call<'a>(&'a self, args: serde_json::Value, _ctx: &'a CallContext) -> CallFuture<'a> {
492 Box::pin(async move { Ok(json!({"echoed": args})) })
493 }
494 }
495
496 fn state_with_echo() -> Arc<ServerState> {
497 let mut reg = Registry::new();
498 reg.register(Arc::new(EchoStub::new()));
499 Arc::new(ServerState {
500 registry: reg,
501 config: SharedServerConfig::for_test(),
502 tier_policy: TierPolicy::defaults(),
503 middleware: vec![],
504 metrics: Arc::new(atd_runtime::MetricsCounters::default()),
505 cursor_issuer: Arc::new(atd_runtime::cursor::CursorIssuer::new([0u8; 32])),
506 })
507 }
508
509 async fn body_to_json(resp: axum::response::Response) -> Value {
510 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
511 serde_json::from_slice(&bytes).unwrap()
512 }
513
514 #[tokio::test]
515 async fn initialize_returns_server_info_with_tools_capability() {
516 let resp = handle_initialize(Some(json!(1)), "atd-test 0.3.0", None);
517 let body = body_to_json(resp).await;
518 assert_eq!(body["jsonrpc"], "2.0");
519 assert_eq!(body["id"], 1);
520 assert_eq!(body["result"]["serverInfo"]["name"], "atd-test");
521 assert_eq!(body["result"]["serverInfo"]["version"], "0.3.0");
522 assert!(body["result"]["capabilities"]["tools"].is_object());
523 assert_eq!(body["result"]["protocolVersion"], "2025-06-18");
524 assert!(body["result"]["capabilities"]["experimental"].is_null());
526 }
527
528 #[tokio::test]
529 async fn initialize_echoes_broker_accepted_token_formats_when_wired() {
530 use atd_runtime::secrets::{ResolveBearerFuture, ResolveFuture, TokenBroker};
531 #[derive(Debug)]
532 struct DeclaringBroker;
533 impl TokenBroker for DeclaringBroker {
534 fn resolve<'a>(&'a self, _caller_id: Option<&'a str>) -> ResolveFuture<'a> {
535 Box::pin(async { Ok(None) })
536 }
537 fn resolve_bearer<'a>(&'a self, _bearer: &'a str) -> ResolveBearerFuture<'a> {
538 Box::pin(async { Ok(None) })
539 }
540 fn accepted_token_formats(&self) -> &'static [&'static str] {
541 &["ucan-jwt", "opaque", "ce-pairing-code"]
542 }
543 }
544 let broker: Arc<dyn TokenBroker> = Arc::new(DeclaringBroker);
545 let resp = handle_initialize(Some(json!(1)), "atd-test 0.3.0", Some(&broker));
546 let body = body_to_json(resp).await;
547 let formats =
548 &body["result"]["capabilities"]["experimental"]["atd"]["acceptedTokenFormats"];
549 assert_eq!(
550 formats,
551 &json!(["ucan-jwt", "opaque", "ce-pairing-code"]),
552 "broker formats should appear in initialize.capabilities.experimental.atd"
553 );
554 }
555
556 #[tokio::test]
557 async fn initialize_omits_experimental_when_broker_declares_empty_formats() {
558 use atd_runtime::secrets::{ResolveBearerFuture, ResolveFuture, TokenBroker};
559 #[derive(Debug)]
560 struct SilentBroker;
561 impl TokenBroker for SilentBroker {
562 fn resolve<'a>(&'a self, _caller_id: Option<&'a str>) -> ResolveFuture<'a> {
563 Box::pin(async { Ok(None) })
564 }
565 fn resolve_bearer<'a>(&'a self, _bearer: &'a str) -> ResolveBearerFuture<'a> {
566 Box::pin(async { Ok(None) })
567 }
568 }
571 let broker: Arc<dyn TokenBroker> = Arc::new(SilentBroker);
572 let resp = handle_initialize(Some(json!(1)), "atd-test 0.3.0", Some(&broker));
573 let body = body_to_json(resp).await;
574 assert!(body["result"]["capabilities"]["experimental"].is_null());
575 }
576
577 #[tokio::test]
578 async fn tools_list_advertises_registered_tools() {
579 let state = state_with_echo();
580 let resp = handle_tools_list(Some(json!(2)), &state);
581 let body = body_to_json(resp).await;
582 let tools = body["result"]["tools"].as_array().unwrap();
583 assert_eq!(tools.len(), 1);
584 assert_eq!(tools[0]["name"], "ref:echo.say");
585 assert!(tools[0]["inputSchema"].is_object());
586 }
587
588 #[tokio::test]
589 async fn tools_call_echo_returns_is_error_false_with_serialised_result() {
590 let state = state_with_echo();
591 let params = json!({"name": "ref:echo.say", "arguments": {"hi": "x"}});
592 let resp = handle_tools_call(Some(json!(3)), &state, None, params).await;
593 let body = body_to_json(resp).await;
594 assert_eq!(body["result"]["isError"], false);
595 let text = body["result"]["content"][0]["text"].as_str().unwrap();
596 let parsed: Value = serde_json::from_str(text).unwrap();
597 assert_eq!(parsed["echoed"]["hi"], "x");
598 }
599
600 #[tokio::test]
601 async fn tools_call_missing_name_returns_invalid_params_error() {
602 let state = state_with_echo();
603 let params = json!({"arguments": {}});
604 let resp = handle_tools_call(Some(json!(4)), &state, None, params).await;
605 let body = body_to_json(resp).await;
606 assert_eq!(body["error"]["code"], -32602);
607 assert!(
608 body["error"]["message"]
609 .as_str()
610 .unwrap()
611 .contains("missing")
612 );
613 }
614
615 #[tokio::test]
616 async fn tools_call_unknown_tool_returns_error_envelope() {
617 let state = state_with_echo();
618 let params = json!({"name": "ref:does-not-exist", "arguments": {}});
619 let resp = handle_tools_call(Some(json!(5)), &state, None, params).await;
620 let body = body_to_json(resp).await;
621 assert_eq!(body["error"]["code"], -32603);
623 assert!(
624 body["error"]["message"]
625 .as_str()
626 .unwrap()
627 .contains("tool not found")
628 );
629 }
630
631 struct PageStubHttp {
638 def: atd_protocol::ToolDefinition,
639 }
640 impl PageStubHttp {
641 fn new() -> Self {
642 Self {
643 def: stub_def("ref:page_stub"),
644 }
645 }
646 }
647 impl Tool for PageStubHttp {
648 fn definition(&self) -> &atd_protocol::ToolDefinition {
649 &self.def
650 }
651 fn supports_pagination(&self) -> bool {
652 true
653 }
654 fn call<'a>(&'a self, _args: serde_json::Value, _ctx: &'a CallContext) -> CallFuture<'a> {
655 Box::pin(async { Ok(json!({"unused": true})) })
656 }
657 fn call_paginated<'a>(
658 &'a self,
659 _args: serde_json::Value,
660 ctx: &'a CallContext,
661 cursor: Option<&'a str>,
662 ) -> atd_runtime::PaginatedCallFuture<'a> {
663 let issuer = ctx.cursor_issuer().expect("issuer attached");
664 let page = match cursor {
665 None => 1u32,
666 Some(c) => {
667 let p = issuer.verify(c, 300).expect("pre-verified");
668 u32::from_be_bytes(p.opaque_state[..4].try_into().unwrap())
669 }
670 };
671 let payload = atd_runtime::cursor::CursorPayload {
672 tool_id: "ref:page_stub".into(),
673 caller_id: ctx.caller_id.clone(),
674 args_fingerprint: [0u8; 32],
675 page_index: page + 1,
676 issued_at_unix: std::time::SystemTime::now()
677 .duration_since(std::time::UNIX_EPOCH)
678 .unwrap()
679 .as_secs(),
680 server_session: issuer.session_nonce(),
681 opaque_state: (page + 1).to_be_bytes().to_vec(),
682 };
683 let next = issuer.issue(payload).ok();
684 Box::pin(async move {
685 Ok(atd_runtime::PaginatedResult {
686 value: json!({"page": page}),
687 next_cursor: next,
688 })
689 })
690 }
691 }
692
693 fn state_with_page_stub() -> Arc<ServerState> {
694 let mut reg = Registry::new();
695 reg.register(Arc::new(PageStubHttp::new()));
696 Arc::new(ServerState {
697 registry: reg,
698 config: SharedServerConfig::for_test(),
699 tier_policy: TierPolicy::defaults(),
700 middleware: vec![],
701 metrics: Arc::new(atd_runtime::MetricsCounters::default()),
702 cursor_issuer: Arc::new(atd_runtime::cursor::CursorIssuer::new([0u8; 32])),
703 })
704 }
705
706 #[tokio::test]
707 async fn tools_call_initial_paginated_call_surfaces_next_cursor_in_envelope() {
708 let state = state_with_page_stub();
709 let params = json!({"name": "ref:page_stub", "arguments": {}});
713 let resp = handle_tools_call(Some(json!(7)), &state, None, params).await;
714 let body = body_to_json(resp).await;
715 assert_eq!(body["result"]["isError"], false);
716 assert!(
717 body["result"]["nextCursor"].is_string(),
718 "paginating tool on first call must emit nextCursor; got: {body}"
719 );
720 }
721
722 #[tokio::test]
723 async fn tools_call_with_dunder_cursor_routes_to_run_tool_continue() {
724 let state = state_with_page_stub();
725 let payload = atd_runtime::cursor::CursorPayload {
728 tool_id: "ref:page_stub".into(),
729 caller_id: None,
730 args_fingerprint: [0u8; 32],
731 page_index: 2,
732 issued_at_unix: std::time::SystemTime::now()
733 .duration_since(std::time::UNIX_EPOCH)
734 .unwrap()
735 .as_secs(),
736 server_session: state.cursor_issuer.session_nonce(),
737 opaque_state: 2u32.to_be_bytes().to_vec(),
738 };
739 let cursor = state.cursor_issuer.issue(payload).unwrap();
740
741 let params = json!({
743 "name": "ref:page_stub",
744 "arguments": {"__cursor": cursor},
745 });
746 let resp = handle_tools_call(Some(json!(8)), &state, None, params).await;
747 let body = body_to_json(resp).await;
748 assert_eq!(body["result"]["isError"], false);
749 let text = body["result"]["content"][0]["text"].as_str().unwrap();
751 let parsed: Value = serde_json::from_str(text).unwrap();
752 assert_eq!(parsed["page"], 2);
753 assert!(
755 body["result"]["nextCursor"].is_string(),
756 "continuation must emit nextCursor; got: {body}"
757 );
758 }
759
760 #[tokio::test]
761 async fn tools_call_expired_cursor_returns_jsonrpc_error() {
762 let state = state_with_page_stub();
763 let payload = atd_runtime::cursor::CursorPayload {
765 tool_id: "ref:page_stub".into(),
766 caller_id: None,
767 args_fingerprint: [0u8; 32],
768 page_index: 2,
769 issued_at_unix: 1, server_session: state.cursor_issuer.session_nonce(),
771 opaque_state: 2u32.to_be_bytes().to_vec(),
772 };
773 let cursor = state.cursor_issuer.issue(payload).unwrap();
774 let params = json!({
775 "name": "ref:page_stub",
776 "arguments": {"__cursor": cursor},
777 });
778 let resp = handle_tools_call(Some(json!(9)), &state, None, params).await;
779 let body = body_to_json(resp).await;
780 assert_eq!(body["error"]["code"], -32603);
782 assert_eq!(body["error"]["data"]["atd_code"], 1020);
783 }
784}