Skip to main content

mesh_llm_plugin/
dsl.rs

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}