Skip to main content

x402_extensions/
bazaar.rs

1//! The `bazaar` extension for resource discovery and cataloging.
2//!
3//! The `bazaar` extension enables resource servers to declare their endpoint
4//! specifications (HTTP method or MCP tool name, input parameters, and output format)
5//! so that facilitators can catalog and index them in a discovery service.
6//!
7//! # Example: GET Endpoint
8//!
9//! ```
10//! use x402_extensions::bazaar::*;
11//! use x402_core::types::Extension;
12//! use serde_json::json;
13//!
14//! let info = BazaarInfo::builder()
15//!     .input(BazaarInput::Http(BazaarHttpInput::builder()
16//!         .method(HttpMethod::GET)
17//!         .query_params(json!({"city": "San Francisco"}))
18//!         .build()))
19//!     .output(BazaarOutput::builder()
20//!         .output_type("json")
21//!         .example(json!({"city": "San Francisco", "weather": "foggy"}))
22//!         .build())
23//!     .build();
24//!
25//! let ext = Extension::typed(info);
26//! let (key, transport) = ext.into_pair();
27//! assert_eq!(key, "bazaar");
28//! ```
29//!
30//! # Example: POST Endpoint
31//!
32//! ```
33//! use x402_extensions::bazaar::*;
34//! use x402_core::types::Extension;
35//! use serde_json::json;
36//!
37//! let info = BazaarInfo::builder()
38//!     .input(BazaarInput::Http(BazaarHttpInput::builder()
39//!         .method(HttpMethod::POST)
40//!         .body_type("json")
41//!         .body(json!({"query": "example"}))
42//!         .build()))
43//!     .build();
44//!
45//! let ext = Extension::typed(info);
46//! let (key, _) = ext.into_pair();
47//! assert_eq!(key, "bazaar");
48//! ```
49//!
50//! # Example: MCP Tool
51//!
52//! ```
53//! use x402_extensions::bazaar::*;
54//! use x402_core::types::Extension;
55//! use serde_json::json;
56//!
57//! let info = BazaarInfo::builder()
58//!     .input(BazaarInput::Mcp(BazaarMcpInput::builder()
59//!         .tool("financial_analysis")
60//!         .input_schema(json!({
61//!             "type": "object",
62//!             "properties": {
63//!                 "ticker": { "type": "string" }
64//!             },
65//!             "required": ["ticker"]
66//!         }))
67//!         .description("AI-powered financial analysis")
68//!         .build()))
69//!     .output(BazaarOutput::builder()
70//!         .output_type("json")
71//!         .example(json!({"summary": "Strong fundamentals", "score": 8.5}))
72//!         .build())
73//!     .build();
74//!
75//! let ext = Extension::typed(info);
76//! let (key, _) = ext.into_pair();
77//! assert_eq!(key, "bazaar");
78//! ```
79
80use bon::Builder;
81use schemars::JsonSchema;
82use serde::{Deserialize, Serialize};
83use x402_core::types::{AnyJson, ExtensionInfo};
84
85/// Discovery info for the `bazaar` extension.
86///
87/// Contains the input specification and optional output description
88/// for a resource server endpoint.
89#[derive(Builder, Debug, Clone, Serialize, Deserialize, JsonSchema)]
90pub struct BazaarInfo {
91    /// How to call the endpoint or tool.
92    pub input: BazaarInput,
93
94    /// Expected response format (optional).
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub output: Option<BazaarOutput>,
97}
98
99impl ExtensionInfo for BazaarInfo {
100    const ID: &'static str = "bazaar";
101
102    fn schema() -> AnyJson {
103        let schema = schemars::schema_for!(BazaarInfo);
104        serde_json::to_value(&schema).expect("BazaarInfo schema generation should not fail")
105    }
106}
107
108/// Discriminated union for input types.
109///
110/// - `Http`: HTTP endpoints (GET, HEAD, DELETE, POST, PUT, PATCH)
111/// - `Mcp`: MCP (Model Context Protocol) tools
112#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
113#[serde(tag = "type")]
114pub enum BazaarInput {
115    /// HTTP endpoint input.
116    #[serde(rename = "http")]
117    Http(BazaarHttpInput),
118
119    /// MCP tool input.
120    #[serde(rename = "mcp")]
121    Mcp(BazaarMcpInput),
122}
123
124/// HTTP endpoint input specification.
125///
126/// For query parameter methods (GET, HEAD, DELETE), use `query_params` and `headers`.
127/// For body methods (POST, PUT, PATCH), additionally use `body_type` and `body`.
128#[derive(Builder, Debug, Clone, Serialize, Deserialize, JsonSchema)]
129#[serde(rename_all = "camelCase")]
130pub struct BazaarHttpInput {
131    /// HTTP method (GET, HEAD, DELETE, POST, PUT, PATCH).
132    pub method: HttpMethod,
133
134    /// Query parameter examples.
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub query_params: Option<AnyJson>,
137
138    /// Custom header examples.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub headers: Option<AnyJson>,
141
142    /// Request body content type. Required for body methods (POST, PUT, PATCH).
143    /// One of `"json"`, `"form-data"`, `"text"`.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    #[builder(into)]
146    pub body_type: Option<String>,
147
148    /// Request body example. Required for body methods (POST, PUT, PATCH).
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub body: Option<AnyJson>,
151}
152
153/// HTTP methods supported by the bazaar extension.
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
155pub enum HttpMethod {
156    /// HTTP GET method.
157    GET,
158    /// HTTP HEAD method.
159    HEAD,
160    /// HTTP DELETE method.
161    DELETE,
162    /// HTTP POST method.
163    POST,
164    /// HTTP PUT method.
165    PUT,
166    /// HTTP PATCH method.
167    PATCH,
168}
169
170/// MCP tool input specification.
171///
172/// Describes an MCP tool's name, input schema, and optional metadata.
173#[derive(Builder, Debug, Clone, Serialize, Deserialize, JsonSchema)]
174#[serde(rename_all = "camelCase")]
175pub struct BazaarMcpInput {
176    /// MCP tool name (matches what's passed to `tools/call`).
177    #[builder(into)]
178    pub tool: String,
179
180    /// JSON Schema for the tool's `arguments`, following the MCP `Tool.inputSchema` format.
181    pub input_schema: AnyJson,
182
183    /// Human-readable description of the tool.
184    #[serde(skip_serializing_if = "Option::is_none")]
185    #[builder(into)]
186    pub description: Option<String>,
187
188    /// MCP transport protocol. One of `"streamable-http"` or `"sse"`.
189    /// Defaults to `"streamable-http"` if omitted.
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub transport: Option<McpTransport>,
192
193    /// Example `arguments` object.
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub example: Option<AnyJson>,
196}
197
198/// MCP transport protocol options.
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
200#[serde(rename_all = "kebab-case")]
201pub enum McpTransport {
202    /// Streamable HTTP transport (default).
203    StreamableHttp,
204    /// Server-Sent Events transport.
205    Sse,
206}
207
208/// Output specification for a bazaar discovery entry.
209#[derive(Builder, Debug, Clone, Serialize, Deserialize, JsonSchema)]
210pub struct BazaarOutput {
211    /// Response content type (e.g., `"json"`, `"text"`).
212    #[serde(rename = "type")]
213    #[builder(into)]
214    pub output_type: String,
215
216    /// Additional format information.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    #[builder(into)]
219    pub format: Option<String>,
220
221    /// Example response value.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub example: Option<AnyJson>,
224}
225
226#[cfg(test)]
227mod tests {
228    use serde_json::json;
229    use x402_core::types::{Extension, ExtensionMapInsert, Record};
230
231    use super::*;
232
233    #[test]
234    fn bazaar_get_endpoint() {
235        let info = BazaarInfo::builder()
236            .input(BazaarInput::Http(
237                BazaarHttpInput::builder()
238                    .method(HttpMethod::GET)
239                    .query_params(json!({"city": "San Francisco"}))
240                    .build(),
241            ))
242            .output(
243                BazaarOutput::builder()
244                    .output_type("json")
245                    .example(json!({
246                        "city": "San Francisco",
247                        "weather": "foggy",
248                        "temperature": 60
249                    }))
250                    .build(),
251            )
252            .build();
253
254        let ext = Extension::typed(info);
255        let (key, transport_ext) = ext.into_pair();
256
257        assert_eq!(key, "bazaar");
258
259        let info_json = &transport_ext.info;
260        assert_eq!(info_json["input"]["type"], "http");
261        assert_eq!(info_json["input"]["method"], "GET");
262        assert_eq!(
263            info_json["input"]["queryParams"],
264            json!({"city": "San Francisco"})
265        );
266        assert_eq!(info_json["output"]["type"], "json");
267    }
268
269    #[test]
270    fn bazaar_post_endpoint() {
271        let info = BazaarInfo::builder()
272            .input(BazaarInput::Http(
273                BazaarHttpInput::builder()
274                    .method(HttpMethod::POST)
275                    .body_type("json")
276                    .body(json!({"query": "example"}))
277                    .build(),
278            ))
279            .output(
280                BazaarOutput::builder()
281                    .output_type("json")
282                    .example(json!({"results": []}))
283                    .build(),
284            )
285            .build();
286
287        let ext = Extension::typed(info);
288        let (key, transport_ext) = ext.into_pair();
289
290        assert_eq!(key, "bazaar");
291
292        let info_json = &transport_ext.info;
293        assert_eq!(info_json["input"]["type"], "http");
294        assert_eq!(info_json["input"]["method"], "POST");
295        assert_eq!(info_json["input"]["bodyType"], "json");
296        assert_eq!(info_json["input"]["body"], json!({"query": "example"}));
297    }
298
299    #[test]
300    fn bazaar_mcp_tool() {
301        let info = BazaarInfo::builder()
302            .input(BazaarInput::Mcp(
303                BazaarMcpInput::builder()
304                    .tool("financial_analysis")
305                    .input_schema(json!({
306                        "type": "object",
307                        "properties": {
308                            "ticker": { "type": "string" },
309                            "analysis_type": { "type": "string", "enum": ["quick", "deep"] }
310                        },
311                        "required": ["ticker"]
312                    }))
313                    .description("Advanced AI-powered financial analysis")
314                    .example(json!({
315                        "ticker": "AAPL",
316                        "analysis_type": "deep"
317                    }))
318                    .build(),
319            ))
320            .output(
321                BazaarOutput::builder()
322                    .output_type("json")
323                    .example(json!({
324                        "summary": "Strong fundamentals...",
325                        "score": 8.5
326                    }))
327                    .build(),
328            )
329            .build();
330
331        let ext = Extension::typed(info);
332        let (key, transport_ext) = ext.into_pair();
333
334        assert_eq!(key, "bazaar");
335
336        let info_json = &transport_ext.info;
337        assert_eq!(info_json["input"]["type"], "mcp");
338        assert_eq!(info_json["input"]["tool"], "financial_analysis");
339        assert!(info_json["input"]["inputSchema"].is_object());
340    }
341
342    #[test]
343    fn bazaar_mcp_with_transport() {
344        let info = BazaarInfo::builder()
345            .input(BazaarInput::Mcp(
346                BazaarMcpInput::builder()
347                    .tool("my_tool")
348                    .input_schema(json!({"type": "object"}))
349                    .transport(McpTransport::Sse)
350                    .build(),
351            ))
352            .build();
353
354        let (_, ext) = Extension::typed(info).into_pair();
355        assert_eq!(ext.info["input"]["transport"], "sse");
356    }
357
358    #[test]
359    fn bazaar_schema_is_generated() {
360        let schema = <BazaarInfo as ExtensionInfo>::schema();
361        assert!(schema.is_object());
362        // Schema should define the structure of BazaarInfo
363        let schema_obj = schema.as_object().unwrap();
364        assert!(
365            schema_obj.contains_key("properties") || schema_obj.contains_key("$defs"),
366            "Schema should contain properties or definitions"
367        );
368    }
369
370    #[test]
371    fn bazaar_insert_into_extension_map() {
372        let mut extensions: Record<Extension> = Record::new();
373
374        extensions.insert_typed(Extension::typed(
375            BazaarInfo::builder()
376                .input(BazaarInput::Http(
377                    BazaarHttpInput::builder().method(HttpMethod::GET).build(),
378                ))
379                .build(),
380        ));
381
382        assert!(extensions.contains_key("bazaar"));
383        assert_eq!(extensions["bazaar"].info["input"]["type"], "http");
384        assert_eq!(extensions["bazaar"].info["input"]["method"], "GET");
385    }
386
387    #[test]
388    fn bazaar_roundtrip_serialization() {
389        let info = BazaarInfo::builder()
390            .input(BazaarInput::Http(
391                BazaarHttpInput::builder()
392                    .method(HttpMethod::POST)
393                    .body_type("json")
394                    .body(json!({"key": "value"}))
395                    .headers(json!({"Authorization": "Bearer token"}))
396                    .build(),
397            ))
398            .output(
399                BazaarOutput::builder()
400                    .output_type("json")
401                    .format("utf-8")
402                    .build(),
403            )
404            .build();
405
406        // Serialize to JSON and back
407        let json = serde_json::to_value(&info).unwrap();
408        let deserialized: BazaarInfo = serde_json::from_value(json.clone()).unwrap();
409        let re_serialized = serde_json::to_value(&deserialized).unwrap();
410
411        assert_eq!(json, re_serialized);
412    }
413
414    #[test]
415    fn bazaar_transport_roundtrip() {
416        let info = BazaarInfo::builder()
417            .input(BazaarInput::Mcp(
418                BazaarMcpInput::builder()
419                    .tool("test_tool")
420                    .input_schema(json!({"type": "object"}))
421                    .build(),
422            ))
423            .build();
424
425        let ext = Extension::typed(info);
426        let (key, transport_ext) = ext.into_pair();
427
428        // Serialize the transport extension
429        let json = serde_json::to_value(&transport_ext).unwrap();
430
431        // Deserialize back
432        let deserialized: Extension = serde_json::from_value(json).unwrap();
433
434        assert_eq!(
435            transport_ext.info, deserialized.info,
436            "Info should roundtrip"
437        );
438        assert_eq!(
439            transport_ext.schema, deserialized.schema,
440            "Schema should roundtrip"
441        );
442        assert_eq!(key, "bazaar");
443    }
444}