eb675dfa Andy McCall 2 days ago ↶
1
2
3
4
5
6
7
use super :: Ctx;
8
use anyhow:: { bail, Context, Result } ;
9
use serde_json:: { json, Value as Json} ;
10
use spectra_core:: keys:: KeyPair;
11
use spectra_core:: ledger:: Ledger;
12
use spectra_mcp:: client:: { rpc_result, Client} ;
13
use std:: path:: PathBuf;
14
use std:: sync:: Arc;
15
use tokio:: io:: { AsyncBufReadExt, AsyncWriteExt, BufReader} ;
16
use tokio:: sync:: Mutex;
17
18
19
20
pub const STDIO_VERSIONS : & [ & str ] = & [ " 2025-11-25" , " 2025-06-18" , " 2025-03-26" ] ;
21
22
pub async fn run ( ctx : & Ctx, agent : & str , home : Option< PathBuf> ) -> Result< i32 > {
23
let home = match home {
24
Some ( h) => h,
eca5af92 Andy McCall 1 day ago ↶
25
None if ctx.remote ( ) ? .is_some ( ) => {
26
bail! ( " with Spectra on another machine, name the agent's home: spectra mcp --agent {agent} --home DIR" )
27
}
eb675dfa Andy McCall 2 days ago ↶
28
None => {
29
let ledger = Ledger:: open_read_only( & ctx.db ( ) ? ) .await? ;
30
let h: Option< String> =
31
sqlx:: query_scalar( " SELECT home FROM agents WHERE name = ?" ) .bind ( agent) .fetch_optional ( ledger.pool ( ) ) .await? ;
32
PathBuf:: from( h.with_context ( | | format! ( " no agent named {agent} (spectra agent list)" ) ) ? )
33
}
34
} ;
35
let key = KeyPair:: load_home( & home) .with_context ( | | format! ( " reading {agent} 's key in {} " , home.display ( ) ) ) ? ;
36
if key.identity.name ! = agent {
37
bail! ( " {} holds {}'s key, not {agent}'s" , home.display ( ) , key.identity.name) ;
38
}
eca5af92 Andy McCall 1 day ago ↶
39
let client = Arc:: new( Client:: at( ctx.endpoint ( ) ? , Some ( key) ) ) ;
eb675dfa Andy McCall 2 days ago ↶
40
let version = Arc:: new( Mutex:: new( STDIO_VERSIONS [ 0 ] .to_string ( ) ) ) ;
41
let out = Arc:: new( Mutex:: new( tokio:: io:: stdout( ) ) ) ;
42
let mut lines = BufReader:: new( tokio:: io:: stdin( ) ) .lines ( ) ;
43
let mut tasks = tokio:: task:: JoinSet:: new( ) ;
44
while let Some ( line) = lines.next_line ( ) .await? {
45
if line.trim ( ) .is_empty ( ) {
46
continue ;
47
}
48
let ( client, version, out) = ( client.clone ( ) , version.clone ( ) , out.clone ( ) ) ;
49
50
tasks.spawn ( async move {
51
if let Some ( reply) = forward ( & client, & version, & line) .await {
52
let mut o = out.lock ( ) .await;
53
let _ = o.write_all ( format! ( " {reply} \n " ) .as_bytes ( ) ) .await;
54
let _ = o.flush ( ) .await;
55
}
56
} ) ;
57
}
58
while tasks.join_next ( ) .await.is_some ( ) { }
59
Ok ( 0 )
60
}
61
62
63
async fn forward ( client : & Client, version : & Mutex< String> , line : & str ) -> Option< Json> {
64
let req: Json = match serde_json:: from_str( line) {
65
Ok ( v) => v,
66
Err ( e) => return Some ( json! ( { " jsonrpc" : " 2.0" , " id" : null, " error" : { " code" : - 32700 , " message" : format! ( " not JSON: {e} " ) } } ) ) ,
67
} ;
68
let id = req.get ( " id" ) .cloned ( ) ;
69
let method = req.get ( " method" ) .and_then ( Json:: as_str) .unwrap_or ( " " ) ;
70
let asked_2026 = method = = " server/discover"
71
| | req.pointer ( " /params/_meta/io.modelcontextprotocol~1protocolVersion" ) .and_then ( Json:: as_str) = = Some ( " 2026-07-28" ) ;
72
let v = if asked_2026 {
73
" 2026-07-28" .to_string ( )
74
} else if method = = " initialize" {
75
let asked = req.pointer ( " /params/protocolVersion" ) .and_then ( Json:: as_str) .unwrap_or ( STDIO_VERSIONS [ 2 ] ) ;
76
let chosen = if STDIO_VERSIONS .contains ( & asked) { asked } else { STDIO_VERSIONS [ 0 ] } ;
77
* version.lock ( ) .await = chosen.to_string ( ) ;
78
chosen.to_string ( )
79
} else {
80
version.lock ( ) .await.clone ( )
81
} ;
82
let r = match client.mcp ( & v, & req) .await {
83
Ok ( r) => r,
84
Err ( e) => return id.map ( | id | json! ( { " jsonrpc" : " 2.0" , " id" : id, " error" : { " code" : - 32000 , " message" : format! ( " Spectra is not reachable: {e:#} . Is `spectra serve` running?" ) } } ) ) ,
85
} ;
86
let id = id? ;
87
if r.status = = 202 {
88
return None ;
89
}
90
match rpc_result ( & r) {
91
Ok ( v) => Some ( v) ,
92
Err ( e) => Some (
93
json! ( { " jsonrpc" : " 2.0" , " id" : id, " error" : { " code" : - 32000 , " message" : format! ( " Spectra answered {} : {e:#} " , r.status) } } ) ,
94
) ,
95
}
96
}