1use anyhow::{Result, bail};
2use rmcp::model::{
3 CallToolResult, CancelTaskParams, CancelTaskResult, CompleteRequestParams, CompleteResult,
4 GetPromptRequestParams, GetPromptResult, GetTaskInfoParams, GetTaskPayloadResult,
5 GetTaskResult, GetTaskResultParams, ListPromptsResult, ListResourceTemplatesResult,
6 ListResourcesResult, ListTasksResult, ListToolsResult, PaginatedRequestParams,
7 ReadResourceRequestParams, ReadResourceResult, ServerInfo, SetLevelRequestParams,
8 SubscribeRequestParams, UnsubscribeRequestParams,
9};
10use serde::de::DeserializeOwned;
11use std::collections::HashMap;
12use std::future::Future;
13use std::pin::Pin;
14use std::sync::{Arc, Mutex};
15
16use tokio::sync::{RwLock, mpsc, watch};
17
18use crate::{
19 PROTOCOL_VERSION,
20 context::{
21 PendingHostResponses, PluginContext, drain_pending_host_responses,
22 remove_pending_host_response,
23 },
24 error::{PluginError, PluginResult, PluginRpcResult},
25 helpers::{
26 ToolCallRequest, json_response, parse_get_prompt_request, parse_read_resource_request,
27 parse_rpc_params, parse_tool_call_request,
28 },
29 io::{
30 LocalReadHalf, LocalStream, LocalWriteHalf, connect_from_env, read_envelope_from,
31 write_envelope_to,
32 },
33 proto,
34};
35use serde::{Deserialize, Serialize};
36
37pub use crate::internal_rpc::{InternalRpcPlugin, InternalRpcPluginBuilder};
38pub use crate::simple_plugin::SimplePlugin;
39
40#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
41#[serde(rename_all = "snake_case")]
42pub enum MeshVisibility {
43 #[default]
44 Private,
45 Public,
46}
47
48impl MeshVisibility {
49 fn from_proto(value: i32) -> Self {
50 match proto::MeshVisibility::try_from(value).unwrap_or(proto::MeshVisibility::Unspecified) {
51 proto::MeshVisibility::Public => Self::Public,
52 _ => Self::Private,
53 }
54 }
55}
56
57#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
58#[serde(rename_all = "snake_case")]
59pub enum PluginStartupPolicy {
60 #[default]
61 Any,
62 PrivateMeshOnly,
63 PublicMeshOnly,
64}
65
66#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
67pub struct PluginInitializeRequest {
68 pub host_protocol_version: u32,
69 pub host_version: String,
70 pub host_info_json: String,
71 pub mesh_visibility: MeshVisibility,
72}
73
74impl From<proto::InitializeRequest> for PluginInitializeRequest {
75 fn from(value: proto::InitializeRequest) -> Self {
76 Self {
77 host_protocol_version: value.host_protocol_version,
78 host_version: value.host_version,
79 host_info_json: value.host_info_json,
80 mesh_visibility: MeshVisibility::from_proto(value.mesh_visibility),
81 }
82 }
83}
84
85#[derive(Clone)]
86pub struct PluginMetadata {
87 pub(super) plugin_id: String,
88 pub(super) plugin_version: String,
89 pub(super) server_info: ServerInfo,
90 pub(super) capabilities: Vec<String>,
91 pub(super) manifest: Option<proto::PluginManifest>,
92 pub(super) startup_policy: PluginStartupPolicy,
93}
94
95impl PluginMetadata {
96 pub fn new(
97 plugin_id: impl Into<String>,
98 plugin_version: impl Into<String>,
99 server_info: ServerInfo,
100 ) -> Self {
101 Self {
102 plugin_id: plugin_id.into(),
103 plugin_version: plugin_version.into(),
104 server_info,
105 capabilities: Vec::new(),
106 manifest: None,
107 startup_policy: PluginStartupPolicy::Any,
108 }
109 }
110
111 pub fn with_capabilities(mut self, capabilities: Vec<String>) -> Self {
112 self.capabilities = capabilities;
113 self
114 }
115
116 pub fn with_manifest(mut self, manifest: proto::PluginManifest) -> Self {
117 self.manifest = Some(manifest);
118 self
119 }
120
121 pub fn with_startup_policy(mut self, startup_policy: PluginStartupPolicy) -> Self {
122 self.startup_policy = startup_policy;
123 self
124 }
125}
126
127pub(super) type InitializeFuture<'a> = Pin<Box<dyn Future<Output = PluginResult<()>> + Send + 'a>>;
128pub(super) type InitFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
129pub(super) type HealthFuture<'a> = Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>>;
130pub(super) type OpenStreamFuture<'a> =
131 Pin<Box<dyn Future<Output = PluginResult<Option<proto::OpenStreamResponse>>> + Send + 'a>>;
132pub(super) type RpcMethodFuture<'a> = Pin<Box<dyn Future<Output = PluginRpcResult> + Send + 'a>>;
133pub(super) type SubscribeFuture<'a> = Pin<Box<dyn Future<Output = PluginResult<()>> + Send + 'a>>;
134pub(super) type SetLogLevelFuture<'a> = Pin<Box<dyn Future<Output = PluginResult<()>> + Send + 'a>>;
135
136pub(super) type InitializeHandler = Arc<
137 dyn for<'a, 'ctx> Fn(
138 PluginInitializeRequest,
139 &'a mut PluginContext<'ctx>,
140 ) -> InitializeFuture<'a>
141 + Send
142 + Sync,
143>;
144pub(super) type InitHandler =
145 Arc<dyn for<'a, 'ctx> Fn(&'a mut PluginContext<'ctx>) -> InitFuture<'a> + Send + Sync>;
146pub(super) type HealthHandler =
147 Arc<dyn for<'a, 'ctx> Fn(&'a mut PluginContext<'ctx>) -> HealthFuture<'a> + Send + Sync>;
148pub(super) type SubscribeHandler = Arc<
149 dyn for<'a, 'ctx> Fn(SubscribeRequestParams, &'a mut PluginContext<'ctx>) -> SubscribeFuture<'a>
150 + Send
151 + Sync,
152>;
153pub(super) type UnsubscribeHandler = Arc<
154 dyn for<'a, 'ctx> Fn(
155 UnsubscribeRequestParams,
156 &'a mut PluginContext<'ctx>,
157 ) -> SubscribeFuture<'a>
158 + Send
159 + Sync,
160>;
161pub(super) type SetLogLevelHandler = Arc<
162 dyn for<'a, 'ctx> Fn(
163 SetLevelRequestParams,
164 &'a mut PluginContext<'ctx>,
165 ) -> SetLogLevelFuture<'a>
166 + Send
167 + Sync,
168>;
169pub(super) type ChannelHandler = Arc<
170 dyn for<'a, 'ctx> Fn(proto::ChannelMessage, &'a mut PluginContext<'ctx>) -> InitFuture<'a>
171 + Send
172 + Sync,
173>;
174pub(super) type BulkHandler = Arc<
175 dyn for<'a, 'ctx> Fn(proto::BulkTransferMessage, &'a mut PluginContext<'ctx>) -> InitFuture<'a>
176 + Send
177 + Sync,
178>;
179pub(super) type MeshEventHandler = Arc<
180 dyn for<'a, 'ctx> Fn(proto::MeshEvent, &'a mut PluginContext<'ctx>) -> InitFuture<'a>
181 + Send
182 + Sync,
183>;
184pub(super) type RpcMethodHandler = Arc<
185 dyn for<'a, 'ctx> Fn(proto::RpcRequest, &'a mut PluginContext<'ctx>) -> RpcMethodFuture<'a>
186 + Send
187 + Sync,
188>;
189pub(super) type OpenStreamHandler = Arc<
190 dyn for<'a, 'ctx> Fn(
191 proto::OpenStreamRequest,
192 &'a mut PluginContext<'ctx>,
193 ) -> OpenStreamFuture<'a>
194 + Send
195 + Sync,
196>;
197pub(super) type CancelStreamHandler = Arc<
198 dyn for<'a, 'ctx> Fn(
199 proto::CancelStreamNotification,
200 &'a mut PluginContext<'ctx>,
201 ) -> InitFuture<'a>
202 + Send
203 + Sync,
204>;
205pub(super) type CloseStreamHandler = Arc<
206 dyn for<'a, 'ctx> Fn(
207 proto::CloseStreamNotification,
208 &'a mut PluginContext<'ctx>,
209 ) -> InitFuture<'a>
210 + Send
211 + Sync,
212>;
213pub(super) type StreamErrorHandler = Arc<
214 dyn for<'a, 'ctx> Fn(proto::StreamError, &'a mut PluginContext<'ctx>) -> InitFuture<'a>
215 + Send
216 + Sync,
217>;
218
219#[crate::async_trait]
220pub trait Plugin: Send {
221 fn plugin_id(&self) -> &str;
222 fn plugin_version(&self) -> String;
223 fn server_info(&self) -> ServerInfo;
224
225 fn capabilities(&self) -> Vec<String> {
226 Vec::new()
227 }
228
229 fn manifest(&self) -> Option<proto::PluginManifest> {
230 None
231 }
232
233 async fn initialize(
234 &mut self,
235 _request: PluginInitializeRequest,
236 _context: &mut PluginContext<'_>,
237 ) -> PluginResult<()> {
238 Ok(())
239 }
240
241 async fn on_initialized(&mut self, _context: &mut PluginContext<'_>) -> Result<()> {
242 Ok(())
243 }
244
245 async fn health(&mut self, _context: &mut PluginContext<'_>) -> Result<String> {
246 Ok("ok".into())
247 }
248
249 async fn list_tools(
250 &mut self,
251 _context: &mut PluginContext<'_>,
252 ) -> PluginResult<Option<ListToolsResult>> {
253 Ok(None)
254 }
255
256 async fn call_tool(
257 &mut self,
258 _request: ToolCallRequest,
259 _context: &mut PluginContext<'_>,
260 ) -> PluginResult<Option<CallToolResult>> {
261 Ok(None)
262 }
263
264 async fn list_prompts(
265 &mut self,
266 _request: Option<PaginatedRequestParams>,
267 _context: &mut PluginContext<'_>,
268 ) -> PluginResult<Option<ListPromptsResult>> {
269 Ok(None)
270 }
271
272 async fn get_prompt(
273 &mut self,
274 _request: GetPromptRequestParams,
275 _context: &mut PluginContext<'_>,
276 ) -> PluginResult<Option<GetPromptResult>> {
277 Ok(None)
278 }
279
280 async fn list_resources(
281 &mut self,
282 _request: Option<PaginatedRequestParams>,
283 _context: &mut PluginContext<'_>,
284 ) -> PluginResult<Option<ListResourcesResult>> {
285 Ok(None)
286 }
287
288 async fn read_resource(
289 &mut self,
290 _request: ReadResourceRequestParams,
291 _context: &mut PluginContext<'_>,
292 ) -> PluginResult<Option<ReadResourceResult>> {
293 Ok(None)
294 }
295
296 async fn list_resource_templates(
297 &mut self,
298 _request: Option<PaginatedRequestParams>,
299 _context: &mut PluginContext<'_>,
300 ) -> PluginResult<Option<ListResourceTemplatesResult>> {
301 Ok(None)
302 }
303
304 async fn subscribe_resource(
305 &mut self,
306 _request: SubscribeRequestParams,
307 _context: &mut PluginContext<'_>,
308 ) -> PluginResult<Option<()>> {
309 Ok(None)
310 }
311
312 async fn unsubscribe_resource(
313 &mut self,
314 _request: UnsubscribeRequestParams,
315 _context: &mut PluginContext<'_>,
316 ) -> PluginResult<Option<()>> {
317 Ok(None)
318 }
319
320 async fn complete(
321 &mut self,
322 _request: CompleteRequestParams,
323 _context: &mut PluginContext<'_>,
324 ) -> PluginResult<Option<CompleteResult>> {
325 Ok(None)
326 }
327
328 async fn set_log_level(
329 &mut self,
330 _request: SetLevelRequestParams,
331 _context: &mut PluginContext<'_>,
332 ) -> PluginResult<Option<()>> {
333 Ok(None)
334 }
335
336 async fn list_tasks(
337 &mut self,
338 _request: Option<PaginatedRequestParams>,
339 _context: &mut PluginContext<'_>,
340 ) -> PluginResult<Option<ListTasksResult>> {
341 Ok(None)
342 }
343
344 async fn get_task_info(
345 &mut self,
346 _request: GetTaskInfoParams,
347 _context: &mut PluginContext<'_>,
348 ) -> PluginResult<Option<GetTaskResult>> {
349 Ok(None)
350 }
351
352 async fn get_task_result(
353 &mut self,
354 _request: GetTaskResultParams,
355 _context: &mut PluginContext<'_>,
356 ) -> PluginResult<Option<GetTaskPayloadResult>> {
357 Ok(None)
358 }
359
360 async fn cancel_task(
361 &mut self,
362 _request: CancelTaskParams,
363 _context: &mut PluginContext<'_>,
364 ) -> PluginResult<Option<CancelTaskResult>> {
365 Ok(None)
366 }
367
368 async fn invoke_service(
369 &mut self,
370 request: proto::InvokeServiceRequest,
371 context: &mut PluginContext<'_>,
372 ) -> PluginResult<Option<proto::InvokeServiceResponse>> {
373 match proto::ServiceKind::try_from(request.kind).unwrap_or(proto::ServiceKind::Unspecified)
374 {
375 proto::ServiceKind::Operation => {
376 let arguments = parse_service_input::<serde_json::Value>(&request.input_json)?;
377 let tool_request = ToolCallRequest {
378 name: request.service_name,
379 arguments,
380 };
381 match self.call_tool(tool_request, context).await? {
382 Some(result) => Ok(Some(proto::InvokeServiceResponse {
383 output_json: normalize_call_tool_output(&result)?,
384 is_error: result.is_error.unwrap_or(false),
385 })),
386 None => Ok(None),
387 }
388 }
389 proto::ServiceKind::Prompt => {
390 let params = parse_service_input::<GetPromptRequestParams>(&request.input_json)?;
391 match self.get_prompt(params, context).await? {
392 Some(result) => Ok(Some(proto::InvokeServiceResponse {
393 output_json: serialize_service_output(&result)?,
394 is_error: false,
395 })),
396 None => Ok(None),
397 }
398 }
399 proto::ServiceKind::Resource => {
400 let params = parse_service_input::<ReadResourceRequestParams>(&request.input_json)?;
401 match self.read_resource(params, context).await? {
402 Some(result) => Ok(Some(proto::InvokeServiceResponse {
403 output_json: serialize_service_output(&result)?,
404 is_error: false,
405 })),
406 None => Ok(None),
407 }
408 }
409 proto::ServiceKind::Completion => {
410 let params = parse_service_input::<CompleteRequestParams>(&request.input_json)?;
411 match self.complete(params, context).await? {
412 Some(result) => Ok(Some(proto::InvokeServiceResponse {
413 output_json: serialize_service_output(&result)?,
414 is_error: false,
415 })),
416 None => Ok(None),
417 }
418 }
419 proto::ServiceKind::Unspecified => Err(PluginError::invalid_request(
420 "Service invocation kind is required",
421 )),
422 }
423 }
424
425 async fn handle_rpc(
426 &mut self,
427 request: proto::RpcRequest,
428 context: &mut PluginContext<'_>,
429 ) -> PluginRpcResult {
430 match request.method.as_str() {
431 "tools/list" => match self.list_tools(context).await? {
432 Some(result) => json_response(&result),
433 None => Err(PluginError::method_not_found(
434 "Unsupported MCP method 'tools/list'",
435 )),
436 },
437 "tools/call" => {
438 let tool_call = parse_tool_call_request(&request)?;
439 match self.call_tool(tool_call, context).await? {
440 Some(result) => json_response(&result),
441 None => Err(PluginError::method_not_found(
442 "Unsupported MCP method 'tools/call'",
443 )),
444 }
445 }
446 "prompts/list" => {
447 let params: Option<PaginatedRequestParams> = parse_rpc_params(&request)?;
448 match self.list_prompts(params, context).await? {
449 Some(result) => json_response(&result),
450 None => Err(PluginError::method_not_found(
451 "Unsupported MCP method 'prompts/list'",
452 )),
453 }
454 }
455 "prompts/get" => {
456 let params = parse_get_prompt_request(&request)?;
457 match self.get_prompt(params, context).await? {
458 Some(result) => json_response(&result),
459 None => Err(PluginError::method_not_found(
460 "Unsupported MCP method 'prompts/get'",
461 )),
462 }
463 }
464 "resources/list" => {
465 let params: Option<PaginatedRequestParams> = parse_rpc_params(&request)?;
466 match self.list_resources(params, context).await? {
467 Some(result) => json_response(&result),
468 None => Err(PluginError::method_not_found(
469 "Unsupported MCP method 'resources/list'",
470 )),
471 }
472 }
473 "resources/read" => {
474 let params = parse_read_resource_request(&request)?;
475 match self.read_resource(params, context).await? {
476 Some(result) => json_response(&result),
477 None => Err(PluginError::method_not_found(
478 "Unsupported MCP method 'resources/read'",
479 )),
480 }
481 }
482 "resources/templates/list" => {
483 let params: Option<PaginatedRequestParams> = parse_rpc_params(&request)?;
484 match self.list_resource_templates(params, context).await? {
485 Some(result) => json_response(&result),
486 None => Err(PluginError::method_not_found(
487 "Unsupported MCP method 'resources/templates/list'",
488 )),
489 }
490 }
491 "resources/subscribe" => {
492 let params: SubscribeRequestParams = parse_rpc_params(&request)?;
493 match self.subscribe_resource(params, context).await? {
494 Some(()) => json_response(&serde_json::json!({})),
495 None => Err(PluginError::method_not_found(
496 "Unsupported MCP method 'resources/subscribe'",
497 )),
498 }
499 }
500 "resources/unsubscribe" => {
501 let params: UnsubscribeRequestParams = parse_rpc_params(&request)?;
502 match self.unsubscribe_resource(params, context).await? {
503 Some(()) => json_response(&serde_json::json!({})),
504 None => Err(PluginError::method_not_found(
505 "Unsupported MCP method 'resources/unsubscribe'",
506 )),
507 }
508 }
509 "completion/complete" => {
510 let params: CompleteRequestParams = parse_rpc_params(&request)?;
511 match self.complete(params, context).await? {
512 Some(result) => json_response(&result),
513 None => Err(PluginError::method_not_found(
514 "Unsupported MCP method 'completion/complete'",
515 )),
516 }
517 }
518 "logging/setLevel" => {
519 let params: SetLevelRequestParams = parse_rpc_params(&request)?;
520 match self.set_log_level(params, context).await? {
521 Some(()) => json_response(&serde_json::json!({})),
522 None => Err(PluginError::method_not_found(
523 "Unsupported MCP method 'logging/setLevel'",
524 )),
525 }
526 }
527 "tasks/list" => {
528 let params: Option<PaginatedRequestParams> = parse_rpc_params(&request)?;
529 match self.list_tasks(params, context).await? {
530 Some(result) => json_response(&result),
531 None => Err(PluginError::method_not_found(
532 "Unsupported MCP method 'tasks/list'",
533 )),
534 }
535 }
536 "tasks/get" => {
537 let params: GetTaskInfoParams = parse_rpc_params(&request)?;
538 match self.get_task_info(params, context).await? {
539 Some(result) => json_response(&result),
540 None => Err(PluginError::method_not_found(
541 "Unsupported MCP method 'tasks/get'",
542 )),
543 }
544 }
545 "tasks/result" => {
546 let params: GetTaskResultParams = parse_rpc_params(&request)?;
547 match self.get_task_result(params, context).await? {
548 Some(result) => json_response(&result),
549 None => Err(PluginError::method_not_found(
550 "Unsupported MCP method 'tasks/result'",
551 )),
552 }
553 }
554 "tasks/cancel" => {
555 let params: CancelTaskParams = parse_rpc_params(&request)?;
556 match self.cancel_task(params, context).await? {
557 Some(result) => json_response(&result),
558 None => Err(PluginError::method_not_found(
559 "Unsupported MCP method 'tasks/cancel'",
560 )),
561 }
562 }
563 _ => Err(PluginError::method_not_found(format!(
564 "Unsupported MCP method '{}'",
565 request.method
566 ))),
567 }
568 }
569
570 async fn on_rpc_notification(
571 &mut self,
572 _notification: proto::RpcNotification,
573 _context: &mut PluginContext<'_>,
574 ) -> Result<()> {
575 Ok(())
576 }
577
578 async fn on_channel_message(
579 &mut self,
580 _message: proto::ChannelMessage,
581 _context: &mut PluginContext<'_>,
582 ) -> Result<()> {
583 Ok(())
584 }
585
586 async fn on_bulk_transfer_message(
587 &mut self,
588 _message: proto::BulkTransferMessage,
589 _context: &mut PluginContext<'_>,
590 ) -> Result<()> {
591 Ok(())
592 }
593
594 async fn on_mesh_event(
595 &mut self,
596 _event: proto::MeshEvent,
597 _context: &mut PluginContext<'_>,
598 ) -> Result<()> {
599 Ok(())
600 }
601
602 async fn open_stream(
603 &mut self,
604 _request: proto::OpenStreamRequest,
605 _context: &mut PluginContext<'_>,
606 ) -> PluginResult<Option<proto::OpenStreamResponse>> {
607 Ok(None)
608 }
609
610 async fn on_cancel_stream(
611 &mut self,
612 _notification: proto::CancelStreamNotification,
613 _context: &mut PluginContext<'_>,
614 ) -> Result<()> {
615 Ok(())
616 }
617
618 async fn on_close_stream(
619 &mut self,
620 _notification: proto::CloseStreamNotification,
621 _context: &mut PluginContext<'_>,
622 ) -> Result<()> {
623 Ok(())
624 }
625
626 async fn on_stream_error(
627 &mut self,
628 _error: proto::StreamError,
629 _context: &mut PluginContext<'_>,
630 ) -> Result<()> {
631 Ok(())
632 }
633
634 async fn on_host_error(
635 &mut self,
636 error: proto::ErrorResponse,
637 _context: &mut PluginContext<'_>,
638 ) -> Result<()> {
639 bail!("host error: {}", error.message)
640 }
641}
642
643pub struct PluginRuntime;
644
645struct RuntimeState<P> {
646 plugin: Arc<RwLock<P>>,
647 plugin_id: String,
648 outbound_tx: mpsc::Sender<proto::Envelope>,
649 pending_host_responses: PendingHostResponses,
650}
651
652struct OrderedPayload {
653 request_id: u64,
654 payload: proto::envelope::Payload,
655}
656
657impl PluginRuntime {
658 pub async fn run<P: Plugin + Clone + Sync + 'static>(plugin: P) -> Result<()> {
659 let stream = connect_from_env().await?;
660 Self::run_with_stream(plugin, stream).await
661 }
662
663 pub async fn run_with_stream<P: Plugin + Clone + Sync + 'static>(
664 plugin: P,
665 stream: LocalStream,
666 ) -> Result<()> {
667 let plugin_id = plugin.plugin_id().to_string();
668 let (read, write) = stream.into_split();
669 let (outbound_tx, outbound_rx) = mpsc::channel(256);
670 let (ordered_tx, ordered_rx) = mpsc::channel(256);
671 let (shutdown_tx, shutdown_rx) = watch::channel(false);
672 let state = Arc::new(RuntimeState {
673 plugin: Arc::new(RwLock::new(plugin)),
674 plugin_id,
675 outbound_tx,
676 pending_host_responses: Arc::new(Mutex::new(HashMap::new())),
677 });
678 let mut writer = tokio::spawn(Self::write_loop(
679 write,
680 outbound_rx,
681 state.pending_host_responses.clone(),
682 shutdown_tx.clone(),
683 shutdown_rx.clone(),
684 ));
685 let ordered_handlers = tokio::spawn(Self::ordered_handler_loop(
686 state.clone(),
687 ordered_rx,
688 shutdown_rx,
689 ));
690
691 let read_result = Self::read_loop(state.clone(), read, ordered_tx);
692 tokio::pin!(read_result);
693
694 let result = tokio::select! {
695 read_result = &mut read_result => read_result,
696 writer_result = &mut writer => match writer_result {
697 Ok(Ok(())) => Ok(()),
698 Ok(Err(err)) => Err(err),
699 Err(err) => Err(err.into()),
700 },
701 };
702
703 let shutdown_reason = match &result {
704 Ok(()) => "plugin host connection is closed".to_string(),
705 Err(err) => format!("plugin host connection is closed: {err}"),
706 };
707 Self::shutdown_runtime(&shutdown_tx, &state.pending_host_responses, shutdown_reason);
708 if !writer.is_finished() {
709 let _ = writer.await;
710 }
711 if !ordered_handlers.is_finished() {
712 ordered_handlers.abort();
713 }
714
715 match ordered_handlers.await {
716 Ok(()) => result,
717 Err(err) if err.is_cancelled() => result,
718 Err(err) if result.is_ok() => Err(err.into()),
719 Err(_) => result,
720 }
721 }
722
723 fn shutdown_runtime(
724 shutdown_tx: &watch::Sender<bool>,
725 pending_host_responses: &PendingHostResponses,
726 reason: String,
727 ) {
728 let _ = shutdown_tx.send(true);
729 Self::fail_pending_host_responses(pending_host_responses, reason);
730 }
731
732 fn fail_pending_host_responses(pending_host_responses: &PendingHostResponses, reason: String) {
733 for sender in drain_pending_host_responses(pending_host_responses) {
734 let _ = sender.send(Err(anyhow::anyhow!(reason.clone())));
735 }
736 }
737
738 async fn ordered_handler_loop<P: Plugin + Clone + Sync + 'static>(
739 state: Arc<RuntimeState<P>>,
740 mut ordered_rx: mpsc::Receiver<OrderedPayload>,
741 mut shutdown_rx: watch::Receiver<bool>,
742 ) {
743 loop {
744 tokio::select! {
745 _ = shutdown_rx.changed() => {
746 if *shutdown_rx.borrow() {
747 break;
748 }
749 }
750 payload = ordered_rx.recv() => {
751 let Some(payload) = payload else {
752 break;
753 };
754 let _ = Self::handle_payload(state.clone(), payload.request_id, payload.payload).await;
755 }
756 }
757 }
758 }
759
760 async fn read_loop<P: Plugin + Clone + Sync + 'static>(
761 state: Arc<RuntimeState<P>>,
762 mut read: LocalReadHalf,
763 ordered_tx: mpsc::Sender<OrderedPayload>,
764 ) -> Result<()> {
765 loop {
766 let envelope = read_envelope_from(&mut *read).await?;
767 if Self::complete_pending_host_response(&state, &envelope).await {
768 continue;
769 }
770 if !Self::handle_envelope(state.clone(), &ordered_tx, envelope).await? {
771 break;
772 }
773 }
774 Ok(())
775 }
776
777 async fn write_loop(
778 mut write: LocalWriteHalf,
779 mut outbound_rx: mpsc::Receiver<proto::Envelope>,
780 pending_host_responses: PendingHostResponses,
781 shutdown_tx: watch::Sender<bool>,
782 mut shutdown_rx: watch::Receiver<bool>,
783 ) -> Result<()> {
784 loop {
785 tokio::select! {
786 _ = shutdown_rx.changed() => {
787 if *shutdown_rx.borrow() {
788 Self::drain_outbound_before_shutdown(&mut write, &mut outbound_rx).await?;
789 return Ok(());
790 }
791 }
792 envelope = outbound_rx.recv() => {
793 let Some(envelope) = envelope else {
794 return Ok(());
795 };
796 if let Err(err) = write_envelope_to(&mut *write, &envelope).await {
797 let reason = format!("plugin host write failed: {err}");
798 Self::shutdown_runtime(&shutdown_tx, &pending_host_responses, reason);
799 return Err(err);
800 }
801 }
802 }
803 }
804 }
805
806 async fn drain_outbound_before_shutdown(
807 write: &mut LocalWriteHalf,
808 outbound_rx: &mut mpsc::Receiver<proto::Envelope>,
809 ) -> Result<()> {
810 while let Ok(envelope) = outbound_rx.try_recv() {
811 write_envelope_to(&mut **write, &envelope).await?;
812 }
813 Ok(())
814 }
815
816 async fn complete_pending_host_response<P: Plugin + Clone + Sync>(
817 state: &RuntimeState<P>,
818 envelope: &proto::Envelope,
819 ) -> bool {
820 let Some(sender) =
821 remove_pending_host_response(&state.pending_host_responses, envelope.request_id)
822 else {
823 return false;
824 };
825 let _ = sender.send(Ok(envelope.clone()));
826 true
827 }
828
829 async fn handle_envelope<P: Plugin + Clone + Sync + 'static>(
830 state: Arc<RuntimeState<P>>,
831 ordered_tx: &mpsc::Sender<OrderedPayload>,
832 envelope: proto::Envelope,
833 ) -> Result<bool> {
834 let request_id = envelope.request_id;
835 let Some(payload) = envelope.payload else {
836 return Ok(true);
837 };
838
839 match payload {
840 proto::envelope::Payload::InitializeRequest(request) => {
841 Self::handle_initialize(state, request_id, request).await
842 }
843 proto::envelope::Payload::HealthRequest(_) => Self::handle_health(state, request_id),
844 proto::envelope::Payload::ShutdownRequest(_) => {
845 Self::write_payload(
846 &state,
847 request_id,
848 proto::envelope::Payload::ShutdownResponse(proto::ShutdownResponse {}),
849 )
850 .await?;
851 Ok(false)
852 }
853 payload if Self::is_ordered_payload(&payload) => {
854 Self::enqueue_ordered_payload(ordered_tx, request_id, payload).await
855 }
856 payload => Self::spawn_payload_handler(state, request_id, payload),
857 }
858 }
859
860 fn is_ordered_payload(payload: &proto::envelope::Payload) -> bool {
861 matches!(
862 payload,
863 proto::envelope::Payload::RpcNotification(_)
864 | proto::envelope::Payload::ChannelMessage(_)
865 | proto::envelope::Payload::BulkTransferMessage(_)
866 | proto::envelope::Payload::MeshEvent(_)
867 | proto::envelope::Payload::CancelStreamNotification(_)
868 | proto::envelope::Payload::CloseStreamNotification(_)
869 | proto::envelope::Payload::StreamError(_)
870 | proto::envelope::Payload::ErrorResponse(_)
871 )
872 }
873
874 async fn enqueue_ordered_payload(
875 ordered_tx: &mpsc::Sender<OrderedPayload>,
876 request_id: u64,
877 payload: proto::envelope::Payload,
878 ) -> Result<bool> {
879 ordered_tx
880 .send(OrderedPayload {
881 request_id,
882 payload,
883 })
884 .await
885 .map_err(|_| anyhow::anyhow!("plugin ordered handler is closed"))?;
886 Ok(true)
887 }
888
889 fn spawn_payload_handler<P: Plugin + Clone + Sync + 'static>(
890 state: Arc<RuntimeState<P>>,
891 request_id: u64,
892 payload: proto::envelope::Payload,
893 ) -> Result<bool> {
894 tokio::spawn(async move {
895 let _ = Self::handle_payload(state, request_id, payload).await;
896 });
897 Ok(true)
898 }
899
900 async fn handle_payload<P: Plugin + Clone + Sync>(
901 state: Arc<RuntimeState<P>>,
902 request_id: u64,
903 payload: proto::envelope::Payload,
904 ) -> Result<()> {
905 match payload {
906 proto::envelope::Payload::RpcRequest(request) => {
907 let payload = Self::rpc_payload(&state, request).await;
908 Self::write_payload(&state, request_id, payload).await
909 }
910 proto::envelope::Payload::InvokeServiceRequest(request) => {
911 let payload = Self::invoke_service_payload(&state, request).await;
912 Self::write_payload(&state, request_id, payload).await
913 }
914 proto::envelope::Payload::OpenStreamRequest(request) => {
915 let payload = Self::open_stream_payload(&state, request).await;
916 Self::write_payload(&state, request_id, payload).await
917 }
918 proto::envelope::Payload::RpcNotification(notification) => {
919 Self::handle_rpc_notification(&state, notification)
920 .await
921 .map(|_| ())
922 }
923 proto::envelope::Payload::ChannelMessage(message) => {
924 Self::handle_channel_message(&state, message)
925 .await
926 .map(|_| ())
927 }
928 proto::envelope::Payload::BulkTransferMessage(message) => {
929 Self::handle_bulk_transfer_message(&state, message)
930 .await
931 .map(|_| ())
932 }
933 proto::envelope::Payload::MeshEvent(event) => {
934 Self::handle_mesh_event(&state, event).await.map(|_| ())
935 }
936 proto::envelope::Payload::CancelStreamNotification(notification) => {
937 Self::handle_cancel_stream(&state, notification)
938 .await
939 .map(|_| ())
940 }
941 proto::envelope::Payload::CloseStreamNotification(notification) => {
942 Self::handle_close_stream(&state, notification)
943 .await
944 .map(|_| ())
945 }
946 proto::envelope::Payload::StreamError(error) => {
947 Self::handle_stream_error(&state, error).await.map(|_| ())
948 }
949 proto::envelope::Payload::ErrorResponse(error) => {
950 Self::handle_host_error(&state, error).await.map(|_| ())
951 }
952 _ => Ok(()),
953 }?;
954 Ok(())
955 }
956
957 async fn handle_initialize<P: Plugin + Clone + Sync>(
958 state: Arc<RuntimeState<P>>,
959 request_id: u64,
960 request: proto::InitializeRequest,
961 ) -> Result<bool> {
962 let mut plugin = state.plugin.write().await;
963 let mut context = Self::context(&state);
964 let init_result = plugin
965 .initialize(PluginInitializeRequest::from(request), &mut context)
966 .await;
967 if let Err(err) = init_result {
968 Self::write_payload(
969 &state,
970 request_id,
971 proto::envelope::Payload::ErrorResponse(err.into_error_response()),
972 )
973 .await?;
974 return Ok(false);
975 }
976
977 Self::write_payload(
978 &state,
979 request_id,
980 proto::envelope::Payload::InitializeResponse(proto::InitializeResponse {
981 plugin_id: state.plugin_id.clone(),
982 plugin_protocol_version: PROTOCOL_VERSION,
983 plugin_version: plugin.plugin_version(),
984 server_info_json: serde_json::to_string(&plugin.server_info())?,
985 capabilities: plugin.capabilities(),
986 manifest: plugin.manifest(),
987 }),
988 )
989 .await?;
990
991 let mut context = Self::context(&state);
992 plugin.on_initialized(&mut context).await?;
993 Ok(true)
994 }
995
996 fn handle_health<P: Plugin + Clone + Sync + 'static>(
997 state: Arc<RuntimeState<P>>,
998 request_id: u64,
999 ) -> Result<bool> {
1000 tokio::spawn(async move {
1001 let mut plugin = Self::plugin_for_request(&state).await;
1002 let mut context = Self::context(&state);
1003 let payload = match plugin.health(&mut context).await {
1004 Ok(detail) => proto::envelope::Payload::HealthResponse(proto::HealthResponse {
1005 status: proto::health_response::Status::Ok as i32,
1006 detail,
1007 }),
1008 Err(err) => proto::envelope::Payload::ErrorResponse(
1009 PluginError::internal(format!("health check failed: {err}"))
1010 .into_error_response(),
1011 ),
1012 };
1013 let _ = Self::write_payload(&state, request_id, payload).await;
1014 });
1015 Ok(true)
1016 }
1017
1018 async fn plugin_for_request<P: Plugin + Clone + Sync>(state: &RuntimeState<P>) -> P {
1019 state.plugin.read().await.clone()
1020 }
1021
1022 async fn rpc_payload<P: Plugin + Clone + Sync>(
1023 state: &RuntimeState<P>,
1024 request: proto::RpcRequest,
1025 ) -> proto::envelope::Payload {
1026 let mut plugin = Self::plugin_for_request(state).await;
1027 let mut context = Self::context(state);
1028 match plugin.handle_rpc(request, &mut context).await {
1029 Ok(payload) => payload,
1030 Err(err) => proto::envelope::Payload::ErrorResponse(err.into_error_response()),
1031 }
1032 }
1033
1034 async fn invoke_service_payload<P: Plugin + Clone + Sync>(
1035 state: &RuntimeState<P>,
1036 request: proto::InvokeServiceRequest,
1037 ) -> proto::envelope::Payload {
1038 let mut plugin = Self::plugin_for_request(state).await;
1039 let mut context = Self::context(state);
1040 match plugin.invoke_service(request, &mut context).await {
1041 Ok(Some(response)) => proto::envelope::Payload::InvokeServiceResponse(response),
1042 Ok(None) => proto::envelope::Payload::ErrorResponse(
1043 PluginError::method_not_found("Unsupported service invocation")
1044 .into_error_response(),
1045 ),
1046 Err(err) => proto::envelope::Payload::ErrorResponse(err.into_error_response()),
1047 }
1048 }
1049
1050 async fn open_stream_payload<P: Plugin + Clone + Sync>(
1051 state: &RuntimeState<P>,
1052 request: proto::OpenStreamRequest,
1053 ) -> proto::envelope::Payload {
1054 let mut plugin = Self::plugin_for_request(state).await;
1055 let mut context = Self::context(state);
1056 match plugin.open_stream(request, &mut context).await {
1057 Ok(Some(response)) => proto::envelope::Payload::OpenStreamResponse(response),
1058 Ok(None) => proto::envelope::Payload::ErrorResponse(
1059 PluginError::method_not_found("Unsupported stream control message 'open_stream'")
1060 .into_error_response(),
1061 ),
1062 Err(err) => proto::envelope::Payload::ErrorResponse(err.into_error_response()),
1063 }
1064 }
1065
1066 async fn handle_rpc_notification<P: Plugin + Clone + Sync>(
1067 state: &RuntimeState<P>,
1068 notification: proto::RpcNotification,
1069 ) -> Result<bool> {
1070 let mut plugin = Self::plugin_for_request(state).await;
1071 let mut context = Self::context(state);
1072 plugin
1073 .on_rpc_notification(notification, &mut context)
1074 .await?;
1075 Ok(true)
1076 }
1077
1078 async fn handle_channel_message<P: Plugin + Clone + Sync>(
1079 state: &RuntimeState<P>,
1080 message: proto::ChannelMessage,
1081 ) -> Result<bool> {
1082 let mut plugin = Self::plugin_for_request(state).await;
1083 let mut context = Self::context(state);
1084 plugin.on_channel_message(message, &mut context).await?;
1085 Ok(true)
1086 }
1087
1088 async fn handle_bulk_transfer_message<P: Plugin + Clone + Sync>(
1089 state: &RuntimeState<P>,
1090 message: proto::BulkTransferMessage,
1091 ) -> Result<bool> {
1092 let mut plugin = Self::plugin_for_request(state).await;
1093 let mut context = Self::context(state);
1094 plugin
1095 .on_bulk_transfer_message(message, &mut context)
1096 .await?;
1097 Ok(true)
1098 }
1099
1100 async fn handle_mesh_event<P: Plugin + Clone + Sync>(
1101 state: &RuntimeState<P>,
1102 event: proto::MeshEvent,
1103 ) -> Result<bool> {
1104 let mut plugin = Self::plugin_for_request(state).await;
1105 let mut context = Self::context(state);
1106 plugin.on_mesh_event(event, &mut context).await?;
1107 Ok(true)
1108 }
1109
1110 async fn handle_cancel_stream<P: Plugin + Clone + Sync>(
1111 state: &RuntimeState<P>,
1112 notification: proto::CancelStreamNotification,
1113 ) -> Result<bool> {
1114 let mut plugin = Self::plugin_for_request(state).await;
1115 let mut context = Self::context(state);
1116 plugin.on_cancel_stream(notification, &mut context).await?;
1117 Ok(true)
1118 }
1119
1120 async fn handle_close_stream<P: Plugin + Clone + Sync>(
1121 state: &RuntimeState<P>,
1122 notification: proto::CloseStreamNotification,
1123 ) -> Result<bool> {
1124 let mut plugin = Self::plugin_for_request(state).await;
1125 let mut context = Self::context(state);
1126 plugin.on_close_stream(notification, &mut context).await?;
1127 Ok(true)
1128 }
1129
1130 async fn handle_stream_error<P: Plugin + Clone + Sync>(
1131 state: &RuntimeState<P>,
1132 error: proto::StreamError,
1133 ) -> Result<bool> {
1134 let mut plugin = Self::plugin_for_request(state).await;
1135 let mut context = Self::context(state);
1136 plugin.on_stream_error(error, &mut context).await?;
1137 Ok(true)
1138 }
1139
1140 async fn handle_host_error<P: Plugin + Clone + Sync>(
1141 state: &RuntimeState<P>,
1142 error: proto::ErrorResponse,
1143 ) -> Result<bool> {
1144 let mut plugin = Self::plugin_for_request(state).await;
1145 let mut context = Self::context(state);
1146 plugin.on_host_error(error, &mut context).await?;
1147 Ok(true)
1148 }
1149
1150 fn context<P: Plugin + Clone + Sync>(state: &RuntimeState<P>) -> PluginContext<'static> {
1151 PluginContext::new(
1152 state.plugin_id.clone(),
1153 state.outbound_tx.clone(),
1154 state.pending_host_responses.clone(),
1155 )
1156 }
1157
1158 async fn write_payload<P: Plugin + Clone + Sync>(
1159 state: &RuntimeState<P>,
1160 request_id: u64,
1161 payload: proto::envelope::Payload,
1162 ) -> Result<()> {
1163 state
1164 .outbound_tx
1165 .send(proto::Envelope {
1166 protocol_version: PROTOCOL_VERSION,
1167 plugin_id: state.plugin_id.clone(),
1168 request_id,
1169 payload: Some(payload),
1170 })
1171 .await
1172 .map_err(|_| anyhow::anyhow!("plugin host connection is closed"))
1173 }
1174}
1175
1176fn parse_service_input<T: DeserializeOwned>(input_json: &str) -> PluginResult<T> {
1177 let input = if input_json.trim().is_empty() {
1178 "null"
1179 } else {
1180 input_json
1181 };
1182 serde_json::from_str(input)
1183 .map_err(|err| PluginError::invalid_params(format!("Invalid service input JSON: {err}")))
1184}
1185
1186fn serialize_service_output<T: Serialize>(value: &T) -> PluginResult<String> {
1187 serde_json::to_string(value)
1188 .map_err(|err| PluginError::internal(format!("Serialize service output: {err}")))
1189}
1190
1191fn normalize_call_tool_output(result: &CallToolResult) -> PluginResult<String> {
1192 if let Some(value) = &result.structured_content {
1193 return serialize_service_output(value);
1194 }
1195 if let Some(text) = result.content.first().and_then(|content| content.as_text()) {
1196 return Ok(text.text.clone());
1197 }
1198 serialize_service_output(&result.content)
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203 use super::*;
1204 use crate::{mcp, plugin, plugin_server_info};
1205 use crate::{read_envelope, write_envelope};
1206 use rmcp::model::{
1207 ArgumentInfo, PromptMessage, PromptMessageContent, PromptMessageRole, Reference,
1208 };
1209 use serde_json::json;
1210 use tokio::sync::{Barrier, Notify};
1211 use tokio::time::{Duration, timeout};
1212
1213 #[derive(Clone, Debug, Default, Deserialize, Serialize, schemars::JsonSchema)]
1214 struct DemoArgs {
1215 #[serde(default)]
1216 message: String,
1217 }
1218
1219 fn test_context() -> PluginContext<'static> {
1220 let (outbound_tx, _outbound_rx) = mpsc::channel(8);
1221 PluginContext::new(
1222 "demo".into(),
1223 outbound_tx,
1224 Arc::new(Mutex::new(HashMap::new())),
1225 )
1226 }
1227
1228 fn test_channel_message(message_kind: &str) -> proto::ChannelMessage {
1229 proto::ChannelMessage {
1230 channel: "events".into(),
1231 source_peer_id: "peer-a".into(),
1232 target_peer_id: "peer-b".into(),
1233 content_type: "text/plain".into(),
1234 body: Vec::new(),
1235 message_kind: message_kind.into(),
1236 correlation_id: String::new(),
1237 metadata_json: String::new(),
1238 }
1239 }
1240
1241 #[tokio::test]
1242 async fn invoke_service_dispatches_operation_prompt_resource_and_completion() {
1243 let mut plugin = plugin! {
1244 metadata: PluginMetadata::new(
1245 "demo",
1246 "1.0.0",
1247 plugin_server_info("demo", "1.0.0", "Demo", "Demo plugin", None::<String>),
1248 ),
1249 mcp: [
1250 mcp::tool("echo")
1251 .description("Echo input")
1252 .input::<DemoArgs>()
1253 .handle(|args, _context| Box::pin(async move {
1254 Ok(json!({ "echo": args.message }))
1255 })),
1256 mcp::resource("demo://state")
1257 .name("State")
1258 .handle(|request, _context| Box::pin(async move {
1259 Ok(crate::read_resource_result(vec![
1260 rmcp::model::ResourceContents::text("state", request.uri),
1261 ]))
1262 })),
1263 mcp::prompt("brief")
1264 .description("Brief")
1265 .handle(|request, _context| Box::pin(async move {
1266 Ok(crate::get_prompt_result(vec![PromptMessage::new(
1267 PromptMessageRole::User,
1268 PromptMessageContent::text(format!("brief:{}", request.name)),
1269 )]))
1270 })),
1271 mcp::completion("prompt.brief.topic")
1272 .handle(|_request, _context| Box::pin(async move {
1273 crate::complete_result(vec!["alpha".into()])
1274 })),
1275 ],
1276 };
1277
1278 let mut context = test_context();
1279
1280 let op = plugin
1281 .invoke_service(
1282 proto::InvokeServiceRequest {
1283 kind: proto::ServiceKind::Operation as i32,
1284 service_name: "echo".into(),
1285 input_json: json!({ "message": "hello" }).to_string(),
1286 },
1287 &mut context,
1288 )
1289 .await
1290 .unwrap()
1291 .unwrap();
1292 assert_eq!(
1293 serde_json::from_str::<serde_json::Value>(&op.output_json).unwrap(),
1294 json!({"echo": "hello"})
1295 );
1296
1297 let prompt = plugin
1298 .invoke_service(
1299 proto::InvokeServiceRequest {
1300 kind: proto::ServiceKind::Prompt as i32,
1301 service_name: "brief".into(),
1302 input_json: serde_json::to_string(&GetPromptRequestParams::new("brief"))
1303 .unwrap(),
1304 },
1305 &mut context,
1306 )
1307 .await
1308 .unwrap()
1309 .unwrap();
1310 let prompt_result: GetPromptResult = serde_json::from_str(&prompt.output_json).unwrap();
1311 assert_eq!(prompt_result.messages.len(), 1);
1312
1313 let resource = plugin
1314 .invoke_service(
1315 proto::InvokeServiceRequest {
1316 kind: proto::ServiceKind::Resource as i32,
1317 service_name: "demo://state".into(),
1318 input_json: serde_json::to_string(&ReadResourceRequestParams::new(
1319 "demo://state",
1320 ))
1321 .unwrap(),
1322 },
1323 &mut context,
1324 )
1325 .await
1326 .unwrap()
1327 .unwrap();
1328 let resource_result: ReadResourceResult =
1329 serde_json::from_str(&resource.output_json).unwrap();
1330 assert_eq!(resource_result.contents.len(), 1);
1331
1332 let completion = plugin
1333 .invoke_service(
1334 proto::InvokeServiceRequest {
1335 kind: proto::ServiceKind::Completion as i32,
1336 service_name: "brief".into(),
1337 input_json: serde_json::to_string(&CompleteRequestParams::new(
1338 Reference::for_prompt("brief"),
1339 ArgumentInfo {
1340 name: "topic".into(),
1341 value: "a".into(),
1342 },
1343 ))
1344 .unwrap(),
1345 },
1346 &mut context,
1347 )
1348 .await
1349 .unwrap()
1350 .unwrap();
1351 let completion_result: CompleteResult =
1352 serde_json::from_str(&completion.output_json).unwrap();
1353 assert_eq!(
1354 completion_result.completion.values,
1355 vec![String::from("alpha")]
1356 );
1357 }
1358
1359 #[tokio::test]
1360 async fn health_request_returns_while_operation_is_running() {
1361 let started = Arc::new(Notify::new());
1362 let started_for_tool = started.clone();
1363 let plugin = plugin! {
1364 metadata: PluginMetadata::new(
1365 "demo",
1366 "1.0.0",
1367 plugin_server_info("demo", "1.0.0", "Demo", "Demo plugin", None::<String>),
1368 ),
1369 mcp: [
1370 mcp::tool("slow")
1371 .description("Slow operation")
1372 .input::<DemoArgs>()
1373 .handle(move |_args, _context| {
1374 let started = started_for_tool.clone();
1375 Box::pin(async move {
1376 started.notify_one();
1377 tokio::time::sleep(Duration::from_millis(300)).await;
1378 Ok(json!({ "done": true }))
1379 })
1380 }),
1381 ],
1382 };
1383
1384 #[cfg(unix)]
1385 let (plugin_stream, host_stream) = tokio::net::UnixStream::pair().unwrap();
1386 #[cfg(not(unix))]
1387 panic!("runtime stream tests are only implemented for unix");
1388
1389 let runtime = tokio::spawn(PluginRuntime::run_with_stream(
1390 plugin,
1391 LocalStream::Unix(plugin_stream),
1392 ));
1393 let mut host_stream = LocalStream::Unix(host_stream);
1394 write_envelope(
1395 &mut host_stream,
1396 &proto::Envelope {
1397 protocol_version: PROTOCOL_VERSION,
1398 plugin_id: "demo".into(),
1399 request_id: 1,
1400 payload: Some(proto::envelope::Payload::InvokeServiceRequest(
1401 proto::InvokeServiceRequest {
1402 kind: proto::ServiceKind::Operation as i32,
1403 service_name: "slow".into(),
1404 input_json: "{}".into(),
1405 },
1406 )),
1407 },
1408 )
1409 .await
1410 .unwrap();
1411
1412 started.notified().await;
1413 write_envelope(
1414 &mut host_stream,
1415 &proto::Envelope {
1416 protocol_version: PROTOCOL_VERSION,
1417 plugin_id: "demo".into(),
1418 request_id: 2,
1419 payload: Some(proto::envelope::Payload::HealthRequest(
1420 proto::HealthRequest {},
1421 )),
1422 },
1423 )
1424 .await
1425 .unwrap();
1426
1427 let health = timeout(Duration::from_millis(150), read_envelope(&mut host_stream))
1428 .await
1429 .expect("health response should not wait for slow operation")
1430 .unwrap();
1431 assert_eq!(health.request_id, 2);
1432 assert!(matches!(
1433 health.payload,
1434 Some(proto::envelope::Payload::HealthResponse(_))
1435 ));
1436
1437 let invoke = timeout(Duration::from_secs(1), read_envelope(&mut host_stream))
1438 .await
1439 .expect("slow operation should still complete")
1440 .unwrap();
1441 assert_eq!(invoke.request_id, 1);
1442
1443 runtime.abort();
1444 }
1445
1446 #[tokio::test]
1447 async fn invokes_service_requests_concurrently() {
1448 let barrier = Arc::new(Barrier::new(2));
1449 let barrier_for_tool = barrier.clone();
1450 let plugin = plugin! {
1451 metadata: PluginMetadata::new(
1452 "demo",
1453 "1.0.0",
1454 plugin_server_info("demo", "1.0.0", "Demo", "Demo plugin", None::<String>),
1455 ),
1456 mcp: [
1457 mcp::tool("barrier")
1458 .description("Waits for another request")
1459 .input::<DemoArgs>()
1460 .handle(move |_args, _context| {
1461 let barrier = barrier_for_tool.clone();
1462 Box::pin(async move {
1463 barrier.wait().await;
1464 Ok(json!({ "done": true }))
1465 })
1466 }),
1467 ],
1468 };
1469
1470 #[cfg(unix)]
1471 let (plugin_stream, host_stream) = tokio::net::UnixStream::pair().unwrap();
1472 #[cfg(not(unix))]
1473 panic!("runtime stream tests are only implemented for unix");
1474
1475 let runtime = tokio::spawn(PluginRuntime::run_with_stream(
1476 plugin,
1477 LocalStream::Unix(plugin_stream),
1478 ));
1479 let mut host_stream = LocalStream::Unix(host_stream);
1480
1481 for request_id in [1, 2] {
1482 write_envelope(
1483 &mut host_stream,
1484 &proto::Envelope {
1485 protocol_version: PROTOCOL_VERSION,
1486 plugin_id: "demo".into(),
1487 request_id,
1488 payload: Some(proto::envelope::Payload::InvokeServiceRequest(
1489 proto::InvokeServiceRequest {
1490 kind: proto::ServiceKind::Operation as i32,
1491 service_name: "barrier".into(),
1492 input_json: "{}".into(),
1493 },
1494 )),
1495 },
1496 )
1497 .await
1498 .unwrap();
1499 }
1500
1501 let first = timeout(Duration::from_secs(1), read_envelope(&mut host_stream))
1502 .await
1503 .expect("first invoke response should not block behind another request")
1504 .unwrap();
1505 let second = timeout(Duration::from_secs(1), read_envelope(&mut host_stream))
1506 .await
1507 .expect("second invoke response should not block behind another request")
1508 .unwrap();
1509 let mut request_ids = vec![first.request_id, second.request_id];
1510 request_ids.sort_unstable();
1511 assert_eq!(request_ids, vec![1, 2]);
1512
1513 runtime.abort();
1514 }
1515
1516 #[tokio::test]
1517 async fn dropped_open_mesh_stream_request_removes_pending_response() {
1518 let (outbound_tx, mut outbound_rx) = mpsc::channel(8);
1519 let pending_host_responses = Arc::new(Mutex::new(HashMap::new()));
1520 let mut context =
1521 PluginContext::new("demo".into(), outbound_tx, pending_host_responses.clone());
1522
1523 let request = tokio::spawn(async move {
1524 let _ = context
1525 .open_mesh_stream(proto::OpenMeshStreamRequest::default())
1526 .await;
1527 });
1528
1529 let outbound = outbound_rx
1530 .recv()
1531 .await
1532 .expect("request should be sent before awaiting host response");
1533 assert_ne!(outbound.request_id, 0);
1534 assert_eq!(
1535 pending_host_responses
1536 .lock()
1537 .expect("pending map should not be poisoned")
1538 .len(),
1539 1
1540 );
1541
1542 request.abort();
1543 let _ = request.await;
1544
1545 assert!(
1546 pending_host_responses
1547 .lock()
1548 .expect("pending map should not be poisoned")
1549 .is_empty()
1550 );
1551 }
1552
1553 #[tokio::test]
1554 async fn ordered_notifications_do_not_overtake_each_other() {
1555 let first_started = Arc::new(Notify::new());
1556 let release_first = Arc::new(Notify::new());
1557 let handled = Arc::new(Mutex::new(Vec::<String>::new()));
1558 let first_started_for_handler = first_started.clone();
1559 let release_first_for_handler = release_first.clone();
1560 let handled_for_handler = handled.clone();
1561 let plugin = SimplePlugin::new(PluginMetadata::new(
1562 "demo",
1563 "1.0.0",
1564 plugin_server_info("demo", "1.0.0", "Demo", "Demo plugin", None::<String>),
1565 ))
1566 .on_channel_message(move |message, _context| {
1567 let first_started = first_started_for_handler.clone();
1568 let release_first = release_first_for_handler.clone();
1569 let handled = handled_for_handler.clone();
1570 Box::pin(async move {
1571 if message.message_kind == "first" {
1572 first_started.notify_one();
1573 release_first.notified().await;
1574 }
1575 handled
1576 .lock()
1577 .expect("handled list should not be poisoned")
1578 .push(message.message_kind);
1579 Ok(())
1580 })
1581 });
1582
1583 #[cfg(unix)]
1584 let (plugin_stream, host_stream) = tokio::net::UnixStream::pair().unwrap();
1585 #[cfg(not(unix))]
1586 panic!("runtime stream tests are only implemented for unix");
1587
1588 let runtime = tokio::spawn(PluginRuntime::run_with_stream(
1589 plugin,
1590 LocalStream::Unix(plugin_stream),
1591 ));
1592 let mut host_stream = LocalStream::Unix(host_stream);
1593
1594 for (request_id, message_kind) in [(1, "first"), (2, "second")] {
1595 write_envelope(
1596 &mut host_stream,
1597 &proto::Envelope {
1598 protocol_version: PROTOCOL_VERSION,
1599 plugin_id: "demo".into(),
1600 request_id,
1601 payload: Some(proto::envelope::Payload::ChannelMessage(
1602 test_channel_message(message_kind),
1603 )),
1604 },
1605 )
1606 .await
1607 .unwrap();
1608 }
1609
1610 first_started.notified().await;
1611 tokio::time::sleep(Duration::from_millis(50)).await;
1612 assert!(
1613 handled
1614 .lock()
1615 .expect("handled list should not be poisoned")
1616 .is_empty(),
1617 "second message should wait behind the first ordered handler"
1618 );
1619
1620 release_first.notify_one();
1621 timeout(Duration::from_secs(1), async {
1622 loop {
1623 if handled
1624 .lock()
1625 .expect("handled list should not be poisoned")
1626 .len()
1627 == 2
1628 {
1629 break;
1630 }
1631 tokio::time::sleep(Duration::from_millis(10)).await;
1632 }
1633 })
1634 .await
1635 .expect("ordered handlers should finish");
1636
1637 assert_eq!(
1638 *handled.lock().expect("handled list should not be poisoned"),
1639 vec![String::from("first"), String::from("second")]
1640 );
1641
1642 runtime.abort();
1643 }
1644}