1use std::marker::PhantomData;
2use std::sync::Arc;
3
4use schemars::JsonSchema;
5use serde::Serialize;
6use serde::de::DeserializeOwned;
7
8use crate::PluginContext;
9use crate::helpers::{
10 CompletionFuture, CompletionRouter, JsonOperationFuture, OperationRouter, PromptFuture,
11 PromptRouter, ResourceFuture, ResourceRouter, json_schema_operation,
12 prompt as prompt_definition, resource_template as resource_template_definition, text_resource,
13};
14use crate::manifest::{
15 EndpointBuilder, ManifestEntry, PluginManifestBuilder, capability as capability_entry,
16 completion as completion_entry, mcp_http_endpoint, mcp_stdio_endpoint, mcp_tcp_endpoint,
17 mcp_unix_socket_endpoint, openai_http_inference_endpoint, operation as operation_entry,
18 prompt_service as prompt_entry, resource as resource_entry,
19 resource_template_service as resource_template_entry,
20};
21use crate::runtime::{PluginMetadata, SimplePlugin};
22
23fn ensure_description(current: Option<String>, fallback: &str) -> String {
24 current.unwrap_or_else(|| fallback.to_string())
25}
26
27fn normalize_http_operation_name(method: &str, path: &str) -> String {
28 let mut value = format!("__http_{}_{}", method.to_ascii_lowercase(), path);
29 value = value
30 .chars()
31 .map(|ch| {
32 if ch.is_ascii_alphanumeric() {
33 ch.to_ascii_lowercase()
34 } else {
35 '_'
36 }
37 })
38 .collect::<String>();
39 value.trim_matches('_').to_string()
40}
41
42fn template_prefix(uri_template: &str) -> String {
43 uri_template
44 .split('{')
45 .next()
46 .unwrap_or(uri_template)
47 .to_string()
48}
49
50pub struct DeclarativePluginBuilder {
51 metadata: PluginMetadata,
52 manifest: PluginManifestBuilder,
53 operation_router: Option<OperationRouter>,
54 prompt_router: Option<PromptRouter>,
55 resource_router: Option<ResourceRouter>,
56 completion_router: Option<CompletionRouter>,
57 plugin_customizers: Vec<Box<dyn FnOnce(SimplePlugin) -> SimplePlugin>>,
58}
59
60impl DeclarativePluginBuilder {
61 pub fn new(metadata: PluginMetadata) -> Self {
62 Self {
63 metadata,
64 manifest: PluginManifestBuilder::new(),
65 operation_router: None,
66 prompt_router: None,
67 resource_router: None,
68 completion_router: None,
69 plugin_customizers: Vec::new(),
70 }
71 }
72
73 pub fn provide<T: Into<ManifestEntry>>(mut self, item: T) -> Self {
74 match item.into() {
75 ManifestEntry::Capability(capability) => {
76 self.manifest.push_item(capability_entry(capability));
77 }
78 _ => panic!("provides entries must be capabilities"),
79 }
80 self
81 }
82
83 pub fn config_item<T: Into<ManifestEntry>>(mut self, item: T) -> Self {
84 match item.into() {
85 ManifestEntry::ConfigSchema(schema) => self.manifest.push_item(schema),
86 _ => panic!("config entries must be plugin config schemas"),
87 }
88 self
89 }
90
91 pub fn web_ui_item<T: Into<ManifestEntry>>(mut self, item: T) -> Self {
92 match item.into() {
93 ManifestEntry::WebUi(web_ui) => self.manifest.push_item(web_ui),
94 _ => panic!("web_ui entries must be plugin web UI declarations"),
95 }
96 self
97 }
98
99 pub fn mcp_item<T: Into<McpItem>>(mut self, item: T) -> Self {
100 item.into().apply(&mut self);
101 self
102 }
103
104 pub fn http_item<T: Into<HttpItem>>(mut self, item: T) -> Self {
105 item.into().apply(&mut self);
106 self
107 }
108
109 pub fn inference_item<T: Into<InferenceItem>>(mut self, item: T) -> Self {
110 item.into().apply(&mut self);
111 self
112 }
113
114 pub fn mesh_item<T: Into<ManifestEntry>>(mut self, item: T) -> Self {
115 match item.into() {
116 ManifestEntry::MeshChannel(channel) => self.manifest.push_item(channel),
117 _ => panic!("mesh entries must be mesh channels"),
118 }
119 self
120 }
121
122 pub fn event_item<T: Into<ManifestEntry>>(mut self, item: T) -> Self {
123 match item.into() {
124 ManifestEntry::MeshEventSubscription(subscription) => {
125 self.manifest.push_item(subscription);
126 }
127 _ => panic!("event entries must be mesh event subscriptions"),
128 }
129 self
130 }
131
132 pub fn startup_policy(mut self, startup_policy: crate::runtime::PluginStartupPolicy) -> Self {
133 self.metadata = self.metadata.with_startup_policy(startup_policy);
134 self
135 }
136
137 pub fn customize<F>(mut self, customizer: F) -> Self
138 where
139 F: FnOnce(SimplePlugin) -> SimplePlugin + 'static,
140 {
141 self.plugin_customizers.push(Box::new(customizer));
142 self
143 }
144
145 fn ensure_operation_router(&mut self) -> &mut OperationRouter {
146 self.operation_router
147 .get_or_insert_with(OperationRouter::new)
148 }
149
150 fn ensure_prompt_router(&mut self) -> &mut PromptRouter {
151 self.prompt_router.get_or_insert_with(PromptRouter::new)
152 }
153
154 fn ensure_resource_router(&mut self) -> &mut ResourceRouter {
155 self.resource_router.get_or_insert_with(ResourceRouter::new)
156 }
157
158 fn ensure_completion_router(&mut self) -> &mut CompletionRouter {
159 self.completion_router
160 .get_or_insert_with(CompletionRouter::new)
161 }
162
163 pub fn build(self) -> SimplePlugin {
164 let Self {
165 metadata,
166 manifest,
167 operation_router,
168 prompt_router,
169 resource_router,
170 completion_router,
171 plugin_customizers,
172 } = self;
173 let manifest = manifest.build();
174 let mut plugin = SimplePlugin::new(
175 metadata
176 .with_capabilities(manifest.capabilities.clone())
177 .with_manifest(manifest),
178 );
179 if let Some(router) = operation_router {
180 plugin = plugin.with_operation_router(router);
181 }
182 if let Some(router) = prompt_router {
183 plugin = plugin.with_prompt_router(router);
184 }
185 if let Some(router) = resource_router {
186 plugin = plugin.with_resource_router(router);
187 }
188 if let Some(router) = completion_router {
189 plugin = plugin.with_completion_router(router);
190 }
191 for customizer in plugin_customizers {
192 plugin = customizer(plugin);
193 }
194 plugin
195 }
196}
197
198pub enum McpItem {
199 Tool(LocalToolRegistration),
200 Resource(LocalResourceRegistration),
201 ResourceTemplate(LocalResourceTemplateRegistration),
202 Prompt(LocalPromptRegistration),
203 Completion(LocalCompletionRegistration),
204 ExternalEndpoint(EndpointBuilder),
205}
206
207impl McpItem {
208 fn apply(self, builder: &mut DeclarativePluginBuilder) {
209 match self {
210 Self::Tool(item) => {
211 builder.manifest.push_item(item.manifest);
212 (item.register)(builder.ensure_operation_router());
213 }
214 Self::Resource(item) => {
215 builder.manifest.push_item(item.manifest);
216 (item.register)(builder.ensure_resource_router());
217 }
218 Self::ResourceTemplate(item) => {
219 builder.manifest.push_item(item.manifest);
220 (item.register)(builder.ensure_resource_router());
221 }
222 Self::Prompt(item) => {
223 builder.manifest.push_item(item.manifest);
224 (item.register)(builder.ensure_prompt_router());
225 }
226 Self::Completion(item) => {
227 builder.manifest.push_item(item.manifest);
228 (item.register)(builder.ensure_completion_router());
229 }
230 Self::ExternalEndpoint(endpoint) => {
231 builder.manifest.push_item(endpoint);
232 }
233 }
234 }
235}
236
237pub struct McpExternalBuilder {
238 endpoint: EndpointBuilder,
239}
240
241impl McpExternalBuilder {
242 pub fn arg(mut self, arg: impl Into<String>) -> Self {
243 self.endpoint = self.endpoint.arg(arg);
244 self
245 }
246
247 pub fn args<I, S>(mut self, args: I) -> Self
248 where
249 I: IntoIterator<Item = S>,
250 S: Into<String>,
251 {
252 self.endpoint = self.endpoint.args(args);
253 self
254 }
255
256 pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
257 self.endpoint = self.endpoint.namespace(namespace);
258 self
259 }
260
261 pub fn supports_streaming(mut self, supports_streaming: bool) -> Self {
262 self.endpoint = self.endpoint.supports_streaming(supports_streaming);
263 self
264 }
265}
266
267impl From<McpExternalBuilder> for McpItem {
268 fn from(value: McpExternalBuilder) -> Self {
269 Self::ExternalEndpoint(value.endpoint)
270 }
271}
272
273pub enum HttpItem {
274 Route(LocalHttpRouteRegistration),
275}
276
277impl HttpItem {
278 fn apply(self, builder: &mut DeclarativePluginBuilder) {
279 match self {
280 Self::Route(item) => {
281 builder.manifest.push_item(item.operation_manifest);
282 builder.manifest.push_item(item.http_manifest);
283 (item.register)(builder.ensure_operation_router());
284 }
285 }
286 }
287}
288
289pub enum InferenceItem {
290 Endpoint(EndpointBuilder),
291}
292
293pub struct InferenceEndpointBuilder {
294 endpoint: EndpointBuilder,
295}
296
297impl InferenceEndpointBuilder {
298 pub fn managed_by_plugin(mut self, managed_by_plugin: bool) -> Self {
299 self.endpoint = self.endpoint.managed_by_plugin(managed_by_plugin);
300 self
301 }
302
303 pub fn supports_streaming(mut self, supports_streaming: bool) -> Self {
304 self.endpoint = self.endpoint.supports_streaming(supports_streaming);
305 self
306 }
307
308 pub fn protocol(mut self, protocol: impl Into<String>) -> Self {
309 self.endpoint = self.endpoint.protocol(protocol);
310 self
311 }
312}
313
314impl From<InferenceEndpointBuilder> for InferenceItem {
315 fn from(value: InferenceEndpointBuilder) -> Self {
316 Self::Endpoint(value.endpoint)
317 }
318}
319
320impl InferenceItem {
321 fn apply(self, builder: &mut DeclarativePluginBuilder) {
322 match self {
323 Self::Endpoint(endpoint) => {
324 builder.manifest.push_item(endpoint);
325 }
326 }
327 }
328}
329
330type OperationRouterRegistration = Box<dyn Fn(&mut OperationRouter) + Send + Sync>;
331type PromptRouterRegistration = Box<dyn Fn(&mut PromptRouter) + Send + Sync>;
332type ResourceRouterRegistration = Box<dyn Fn(&mut ResourceRouter) + Send + Sync>;
333type CompletionRouterRegistration = Box<dyn Fn(&mut CompletionRouter) + Send + Sync>;
334
335pub struct LocalToolRegistration {
336 manifest: ManifestEntry,
337 register: OperationRouterRegistration,
338}
339
340pub struct LocalResourceRegistration {
341 manifest: ManifestEntry,
342 register: ResourceRouterRegistration,
343}
344
345pub struct LocalResourceTemplateRegistration {
346 manifest: ManifestEntry,
347 register: ResourceRouterRegistration,
348}
349
350pub struct LocalPromptRegistration {
351 manifest: ManifestEntry,
352 register: PromptRouterRegistration,
353}
354
355pub struct LocalCompletionRegistration {
356 manifest: ManifestEntry,
357 register: CompletionRouterRegistration,
358}
359
360pub struct LocalHttpRouteRegistration {
361 operation_manifest: ManifestEntry,
362 http_manifest: ManifestEntry,
363 register: OperationRouterRegistration,
364}
365
366pub mod mcp {
367 use super::*;
368
369 pub fn tool(name: impl Into<String>) -> McpToolBuilder<serde_json::Value> {
370 McpToolBuilder {
371 name: name.into(),
372 description: None,
373 title: None,
374 output_schema_json: None,
375 _input: PhantomData,
376 }
377 }
378
379 pub fn resource(uri: impl Into<String>) -> McpResourceBuilder {
380 McpResourceBuilder {
381 uri: uri.into(),
382 name: None,
383 description: None,
384 mime_type: None,
385 }
386 }
387
388 pub fn resource_template(uri_template: impl Into<String>) -> McpResourceTemplateBuilder {
389 McpResourceTemplateBuilder {
390 uri_template: uri_template.into(),
391 name: None,
392 description: None,
393 mime_type: None,
394 }
395 }
396
397 pub fn prompt(name: impl Into<String>) -> McpPromptBuilder {
398 McpPromptBuilder {
399 name: name.into(),
400 description: None,
401 }
402 }
403
404 pub fn completion(argument_ref: impl Into<String>) -> McpCompletionBuilder {
405 McpCompletionBuilder {
406 argument_ref: argument_ref.into(),
407 description: None,
408 }
409 }
410
411 pub fn external_stdio(
412 endpoint_id: impl Into<String>,
413 command: impl Into<String>,
414 ) -> McpExternalBuilder {
415 McpExternalBuilder {
416 endpoint: mcp_stdio_endpoint(endpoint_id, command),
417 }
418 }
419
420 pub fn external_http(
421 endpoint_id: impl Into<String>,
422 address: impl Into<String>,
423 ) -> McpExternalBuilder {
424 McpExternalBuilder {
425 endpoint: mcp_http_endpoint(endpoint_id, address),
426 }
427 }
428
429 pub fn external_tcp(
430 endpoint_id: impl Into<String>,
431 address: impl Into<String>,
432 ) -> McpExternalBuilder {
433 McpExternalBuilder {
434 endpoint: mcp_tcp_endpoint(endpoint_id, address),
435 }
436 }
437
438 pub fn external_unix_socket(
439 endpoint_id: impl Into<String>,
440 address: impl Into<String>,
441 ) -> McpExternalBuilder {
442 McpExternalBuilder {
443 endpoint: mcp_unix_socket_endpoint(endpoint_id, address),
444 }
445 }
446}
447
448pub mod http {
449 use super::*;
450
451 pub fn get(path: impl Into<String>) -> HttpRouteBuilder<serde_json::Value> {
452 HttpRouteBuilder::new("GET", path)
453 }
454
455 pub fn post(path: impl Into<String>) -> HttpRouteBuilder<serde_json::Value> {
456 HttpRouteBuilder::new("POST", path)
457 }
458
459 pub fn put(path: impl Into<String>) -> HttpRouteBuilder<serde_json::Value> {
460 HttpRouteBuilder::new("PUT", path)
461 }
462
463 pub fn patch(path: impl Into<String>) -> HttpRouteBuilder<serde_json::Value> {
464 HttpRouteBuilder::new("PATCH", path)
465 }
466
467 pub fn delete(path: impl Into<String>) -> HttpRouteBuilder<serde_json::Value> {
468 HttpRouteBuilder::new("DELETE", path)
469 }
470}
471
472pub mod inference {
473 use super::*;
474
475 pub fn openai_http(
476 endpoint_id: impl Into<String>,
477 address: impl Into<String>,
478 ) -> InferenceEndpointBuilder {
479 InferenceEndpointBuilder {
480 endpoint: openai_http_inference_endpoint(endpoint_id, address),
481 }
482 }
483
484 pub fn provider(
485 endpoint_id: impl Into<String>,
486 address: impl Into<String>,
487 ) -> InferenceEndpointBuilder {
488 InferenceEndpointBuilder {
489 endpoint: openai_http_inference_endpoint(endpoint_id, address).managed_by_plugin(true),
490 }
491 }
492}
493
494pub struct McpToolBuilder<TArgs> {
495 name: String,
496 description: Option<String>,
497 title: Option<String>,
498 output_schema_json: Option<String>,
499 _input: PhantomData<TArgs>,
500}
501
502impl<TArgs> McpToolBuilder<TArgs> {
503 pub fn description(mut self, description: impl Into<String>) -> Self {
504 self.description = Some(description.into());
505 self
506 }
507
508 pub fn title(mut self, title: impl Into<String>) -> Self {
509 self.title = Some(title.into());
510 self
511 }
512
513 pub fn input<TNext: DeserializeOwned + JsonSchema + Send + 'static>(
514 self,
515 ) -> McpToolBuilder<TNext> {
516 McpToolBuilder {
517 name: self.name,
518 description: self.description,
519 title: self.title,
520 output_schema_json: self.output_schema_json,
521 _input: PhantomData,
522 }
523 }
524
525 pub fn output<TOutput: JsonSchema>(mut self) -> Self {
526 self.output_schema_json = Some(
527 crate::json_string(&crate::json_schema_for::<TOutput>())
528 .unwrap_or_else(|_| "{}".into()),
529 );
530 self
531 }
532
533 pub fn handle<TResult, F>(self, handler: F) -> McpItem
534 where
535 TArgs: DeserializeOwned + JsonSchema + Send + 'static,
536 TResult: Serialize + Send + 'static,
537 F: for<'a, 'ctx> Fn(TArgs, &'a mut PluginContext<'ctx>) -> JsonOperationFuture<'a, TResult>
538 + Send
539 + Sync
540 + 'static,
541 {
542 let name = self.name.clone();
543 let description = ensure_description(self.description, &name);
544 let title = self.title.clone();
545 let handler = Arc::new(handler);
546 let manifest = operation_entry::<TArgs>(name.clone(), description.clone());
547 let manifest = if let Some(title) = &title {
548 manifest.title(title.clone())
549 } else {
550 manifest
551 };
552 let manifest = match self.output_schema_json {
553 Some(output_schema_json) => {
554 let mut inner =
555 if let ManifestEntry::Operation(inner) = ManifestEntry::from(manifest) {
556 inner
557 } else {
558 unreachable!()
559 };
560 inner.output_schema_json = Some(output_schema_json);
561 ManifestEntry::Operation(inner)
562 }
563 None => manifest.into(),
564 };
565 let register = Box::new(move |router: &mut OperationRouter| {
566 let mut tool = json_schema_operation::<TArgs>(name.clone(), description.clone());
567 if let Some(title) = &title {
568 tool = tool.with_title(title.clone());
569 }
570 let handler = Arc::clone(&handler);
571 router.add_json::<TArgs, TResult, _>(tool, move |args, context| {
572 let handler = Arc::clone(&handler);
573 handler(args, context)
574 });
575 });
576 McpItem::Tool(LocalToolRegistration { manifest, register })
577 }
578}
579
580pub struct McpResourceBuilder {
581 uri: String,
582 name: Option<String>,
583 description: Option<String>,
584 mime_type: Option<String>,
585}
586
587impl McpResourceBuilder {
588 pub fn name(mut self, name: impl Into<String>) -> Self {
589 self.name = Some(name.into());
590 self
591 }
592
593 pub fn description(mut self, description: impl Into<String>) -> Self {
594 self.description = Some(description.into());
595 self
596 }
597
598 pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
599 self.mime_type = Some(mime_type.into());
600 self
601 }
602
603 pub fn handle<F>(self, handler: F) -> McpItem
604 where
605 F: for<'a, 'ctx> Fn(
606 rmcp::model::ReadResourceRequestParams,
607 &'a mut PluginContext<'ctx>,
608 ) -> ResourceFuture<'a>
609 + Send
610 + Sync
611 + 'static,
612 {
613 let uri = self.uri.clone();
614 let name = self.name.unwrap_or_else(|| uri.clone());
615 let description = self.description.clone();
616 let mime_type = self.mime_type.clone();
617 let handler = Arc::new(handler);
618 let mut manifest = resource_entry(uri.clone(), name.clone());
619 if let Some(description) = description.clone() {
620 manifest = manifest.description(description);
621 }
622 if let Some(mime_type) = mime_type.clone() {
623 manifest = manifest.mime_type(mime_type);
624 }
625 let register = Box::new(move |router: &mut ResourceRouter| {
626 let mut resource = text_resource(uri.clone(), name.clone());
627 if let Some(description) = description.clone() {
628 resource.raw.description = Some(description);
629 }
630 if let Some(mime_type) = mime_type.clone() {
631 resource.raw.mime_type = Some(mime_type);
632 }
633 let handler = Arc::clone(&handler);
634 router.add_exact(resource, move |request, context| {
635 let handler = Arc::clone(&handler);
636 handler(request, context)
637 });
638 });
639 McpItem::Resource(LocalResourceRegistration {
640 manifest: manifest.into(),
641 register,
642 })
643 }
644}
645
646pub struct McpResourceTemplateBuilder {
647 uri_template: String,
648 name: Option<String>,
649 description: Option<String>,
650 mime_type: Option<String>,
651}
652
653impl McpResourceTemplateBuilder {
654 pub fn name(mut self, name: impl Into<String>) -> Self {
655 self.name = Some(name.into());
656 self
657 }
658
659 pub fn description(mut self, description: impl Into<String>) -> Self {
660 self.description = Some(description.into());
661 self
662 }
663
664 pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
665 self.mime_type = Some(mime_type.into());
666 self
667 }
668
669 pub fn handle<F>(self, handler: F) -> McpItem
670 where
671 F: for<'a, 'ctx> Fn(
672 rmcp::model::ReadResourceRequestParams,
673 &'a mut PluginContext<'ctx>,
674 ) -> ResourceFuture<'a>
675 + Send
676 + Sync
677 + 'static,
678 {
679 let uri_template = self.uri_template.clone();
680 let name = self.name.unwrap_or_else(|| uri_template.clone());
681 let description = self.description.clone();
682 let mime_type = self.mime_type.clone();
683 let prefix = template_prefix(&uri_template);
684 let handler = Arc::new(handler);
685 let mut manifest = resource_template_entry(uri_template.clone(), name.clone());
686 if let Some(description) = description.clone() {
687 manifest = manifest.description(description);
688 }
689 if let Some(mime_type) = mime_type.clone() {
690 manifest = manifest.mime_type(mime_type);
691 }
692 let register = Box::new(move |router: &mut ResourceRouter| {
693 let mut template = resource_template_definition(uri_template.clone(), name.clone());
694 if let Some(description) = description.clone() {
695 template.raw.description = Some(description);
696 }
697 if let Some(mime_type) = mime_type.clone() {
698 template.raw.mime_type = Some(mime_type);
699 }
700 let handler = Arc::clone(&handler);
701 router.add_prefix_template(template, prefix.clone(), move |request, context| {
702 let handler = Arc::clone(&handler);
703 handler(request, context)
704 });
705 });
706 McpItem::ResourceTemplate(LocalResourceTemplateRegistration {
707 manifest: manifest.into(),
708 register,
709 })
710 }
711}
712
713pub struct McpPromptBuilder {
714 name: String,
715 description: Option<String>,
716}
717
718impl McpPromptBuilder {
719 pub fn description(mut self, description: impl Into<String>) -> Self {
720 self.description = Some(description.into());
721 self
722 }
723
724 pub fn handle<F>(self, handler: F) -> McpItem
725 where
726 F: for<'a, 'ctx> Fn(
727 rmcp::model::GetPromptRequestParams,
728 &'a mut PluginContext<'ctx>,
729 ) -> PromptFuture<'a>
730 + Send
731 + Sync
732 + 'static,
733 {
734 let name = self.name.clone();
735 let description = self.description.clone();
736 let handler = Arc::new(handler);
737 let mut manifest = prompt_entry(name.clone());
738 if let Some(description) = description.clone() {
739 manifest = manifest.description(description);
740 }
741 let register = Box::new(move |router: &mut PromptRouter| {
742 let prompt = prompt_definition(
743 name.clone(),
744 description.clone().unwrap_or_default(),
745 None::<Vec<_>>,
746 );
747 let handler = Arc::clone(&handler);
748 router.add(prompt, move |request, context| {
749 let handler = Arc::clone(&handler);
750 handler(request, context)
751 });
752 });
753 McpItem::Prompt(LocalPromptRegistration {
754 manifest: manifest.into(),
755 register,
756 })
757 }
758}
759
760pub struct McpCompletionBuilder {
761 argument_ref: String,
762 description: Option<String>,
763}
764
765impl McpCompletionBuilder {
766 pub fn description(mut self, description: impl Into<String>) -> Self {
767 self.description = Some(description.into());
768 self
769 }
770
771 pub fn handle<F>(self, handler: F) -> McpItem
772 where
773 F: for<'a, 'ctx> Fn(
774 rmcp::model::CompleteRequestParams,
775 &'a mut PluginContext<'ctx>,
776 ) -> CompletionFuture<'a>
777 + Send
778 + Sync
779 + 'static,
780 {
781 let argument_ref = self.argument_ref.clone();
782 let handler = Arc::new(handler);
783 let mut manifest = completion_entry(argument_ref.clone());
784 if let Some(description) = self.description.clone() {
785 manifest = manifest.description(description);
786 }
787 let register = Box::new(move |router: &mut CompletionRouter| {
788 let handler = Arc::clone(&handler);
789 if let Some((prompt_name, argument_name)) = parse_prompt_argument_ref(&argument_ref) {
790 router.add_prompt_argument(prompt_name, argument_name, move |request, context| {
791 let handler = Arc::clone(&handler);
792 handler(request, context)
793 });
794 }
795 });
796 McpItem::Completion(LocalCompletionRegistration {
797 manifest: manifest.into(),
798 register,
799 })
800 }
801}
802
803pub struct HttpRouteBuilder<TArgs> {
804 method: &'static str,
805 path: String,
806 description: Option<String>,
807 binding_id: Option<String>,
808 request_schema_json: Option<String>,
809 response_schema_json: Option<String>,
810 request_body_mode: i32,
811 response_body_mode: i32,
812 _input: PhantomData<TArgs>,
813}
814
815impl<TArgs> HttpRouteBuilder<TArgs> {
816 fn new(method: &'static str, path: impl Into<String>) -> Self {
817 Self {
818 method,
819 path: path.into(),
820 description: None,
821 binding_id: None,
822 request_schema_json: None,
823 response_schema_json: None,
824 request_body_mode: crate::proto::HttpBodyMode::Buffered as i32,
825 response_body_mode: crate::proto::HttpBodyMode::Buffered as i32,
826 _input: PhantomData,
827 }
828 }
829
830 pub fn description(mut self, description: impl Into<String>) -> Self {
831 self.description = Some(description.into());
832 self
833 }
834
835 pub fn binding_id(mut self, binding_id: impl Into<String>) -> Self {
836 self.binding_id = Some(binding_id.into());
837 self
838 }
839
840 pub fn input<TNext: DeserializeOwned + JsonSchema + Send + 'static>(
841 self,
842 ) -> HttpRouteBuilder<TNext> {
843 HttpRouteBuilder {
844 method: self.method,
845 path: self.path,
846 description: self.description,
847 binding_id: self.binding_id,
848 request_schema_json: Some(
849 crate::json_string(&crate::json_schema_for::<TNext>())
850 .unwrap_or_else(|_| "{}".into()),
851 ),
852 response_schema_json: self.response_schema_json,
853 request_body_mode: self.request_body_mode,
854 response_body_mode: self.response_body_mode,
855 _input: PhantomData,
856 }
857 }
858
859 pub fn output<TOutput: JsonSchema>(mut self) -> Self {
860 self.response_schema_json = Some(
861 crate::json_string(&crate::json_schema_for::<TOutput>())
862 .unwrap_or_else(|_| "{}".into()),
863 );
864 self
865 }
866
867 pub fn stream_request(mut self) -> Self {
868 self.request_body_mode = crate::proto::HttpBodyMode::Streamed as i32;
869 self
870 }
871
872 pub fn stream_response(mut self) -> Self {
873 self.response_body_mode = crate::proto::HttpBodyMode::Streamed as i32;
874 self
875 }
876
877 pub fn sse(mut self) -> Self {
878 self.response_body_mode = crate::proto::HttpBodyMode::Streamed as i32;
879 self
880 }
881
882 pub fn handle<TResult, F>(self, handler: F) -> HttpItem
883 where
884 TArgs: DeserializeOwned + JsonSchema + Send + 'static,
885 TResult: Serialize + Send + 'static,
886 F: for<'a, 'ctx> Fn(TArgs, &'a mut PluginContext<'ctx>) -> JsonOperationFuture<'a, TResult>
887 + Send
888 + Sync
889 + 'static,
890 {
891 let method = self.method;
892 let path = if self.path.starts_with('/') {
893 self.path.clone()
894 } else {
895 format!("/{}", self.path)
896 };
897 let operation_name = self
898 .binding_id
899 .clone()
900 .unwrap_or_else(|| normalize_http_operation_name(method, &path));
901 let description = ensure_description(self.description, &operation_name);
902 let handler = Arc::new(handler);
903
904 let mut op_inner = if let ManifestEntry::Operation(inner) =
905 operation_entry::<TArgs>(operation_name.clone(), description.clone()).into()
906 {
907 inner
908 } else {
909 unreachable!()
910 };
911 op_inner.output_schema_json = self.response_schema_json.clone();
912
913 let mut binding_inner = if let ManifestEntry::HttpBinding(inner) = match method {
914 "GET" => crate::manifest::http_get(path.clone(), operation_name.clone()).into(),
915 "POST" => crate::manifest::http_post(path.clone(), operation_name.clone()).into(),
916 "PUT" => crate::manifest::http_put(path.clone(), operation_name.clone()).into(),
917 "PATCH" => crate::manifest::http_patch(path.clone(), operation_name.clone()).into(),
918 "DELETE" => crate::manifest::http_delete(path.clone(), operation_name.clone()).into(),
919 _ => unreachable!(),
920 } {
921 inner
922 } else {
923 unreachable!()
924 };
925 binding_inner.binding_id = operation_name.clone();
926 binding_inner.request_schema_json = self.request_schema_json.clone();
927 binding_inner.response_schema_json = self.response_schema_json.clone();
928 binding_inner.request_body_mode = self.request_body_mode;
929 binding_inner.response_body_mode = self.response_body_mode;
930
931 let register = Box::new(move |router: &mut OperationRouter| {
932 let handler = Arc::clone(&handler);
933 router.add_json::<TArgs, TResult, _>(
934 json_schema_operation::<TArgs>(operation_name.clone(), description.clone()),
935 move |args, context| {
936 let handler = Arc::clone(&handler);
937 handler(args, context)
938 },
939 );
940 });
941
942 HttpItem::Route(LocalHttpRouteRegistration {
943 operation_manifest: ManifestEntry::Operation(op_inner),
944 http_manifest: ManifestEntry::HttpBinding(binding_inner),
945 register,
946 })
947 }
948}
949
950fn parse_prompt_argument_ref(argument_ref: &str) -> Option<(String, String)> {
951 let remainder = argument_ref.strip_prefix("prompt.")?;
952 let (prompt_name, argument_name) = remainder.rsplit_once('.')?;
953 Some((prompt_name.to_string(), argument_name.to_string()))
954}