#
tokens: 47545/50000 14/231 files (page 4/11)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 of 11. Use http://codebase.md/tuananh/hyper-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .cursor
│   └── rules
│       └── print-ctx-size.mdc
├── .dockerignore
├── .github
│   ├── renovate.json5
│   └── workflows
│       ├── ci.yml
│       ├── nightly.yml
│       └── release.yml
├── .gitignore
├── .gitmodules
├── .hadolint.yaml
├── .pre-commit-config.yaml
├── .windsurf
│   └── rules
│       ├── print-ctx-size.md
│       └── think.md
├── assets
│   ├── cursor-mcp-1.png
│   ├── cursor-mcp.png
│   ├── eval-py.jpg
│   └── logo.png
├── Cargo.lock
├── Cargo.toml
├── config.example.json
├── config.example.yaml
├── CREATING_PLUGINS.md
├── DEPLOYMENT.md
├── Dockerfile
├── examples
│   └── plugins
│       ├── v1
│       │   ├── arxiv
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── context7
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── crates-io
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── crypto-price
│       │   │   ├── Dockerfile
│       │   │   ├── go.mod
│       │   │   ├── go.sum
│       │   │   ├── main.go
│       │   │   ├── pdk.gen.go
│       │   │   └── README.md
│       │   ├── eval-py
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── fetch
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── fs
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── github
│       │   │   ├── .gitignore
│       │   │   ├── branches.go
│       │   │   ├── Dockerfile
│       │   │   ├── files.go
│       │   │   ├── gists.go
│       │   │   ├── go.mod
│       │   │   ├── go.sum
│       │   │   ├── issues.go
│       │   │   ├── main.go
│       │   │   ├── pdk.gen.go
│       │   │   ├── README.md
│       │   │   └── repo.go
│       │   ├── gitlab
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── gomodule
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── hash
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.lock
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── maven
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── meme-generator
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── generate_embedded.py
│       │   │   ├── README.md
│       │   │   ├── src
│       │   │   │   ├── embedded.rs
│       │   │   │   ├── lib.rs
│       │   │   │   └── pdk.rs
│       │   │   └── templates.json
│       │   ├── memory
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── myip
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.lock
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── qdrant
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       ├── pdk.rs
│       │   │       └── qdrant_client.rs
│       │   ├── qr-code
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.lock
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── serper
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── sqlite
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── think
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   └── src
│       │   │       ├── lib.rs
│       │   │       └── pdk.rs
│       │   ├── time
│       │   │   ├── .cargo
│       │   │   │   └── config.toml
│       │   │   ├── .gitignore
│       │   │   ├── Cargo.toml
│       │   │   ├── Dockerfile
│       │   │   ├── README.md
│       │   │   ├── src
│       │   │   │   ├── lib.rs
│       │   │   │   └── pdk.rs
│       │   │   └── time.wasm
│       │   └── tool-list-changed
│       │       ├── .gitignore
│       │       ├── Cargo.toml
│       │       ├── Dockerfile
│       │       ├── README.md
│       │       ├── src
│       │       │   ├── lib.rs
│       │       │   └── pdk.rs
│       │       └── tool_list_changed.wasm
│       └── v2
│           └── rstime
│               ├── .cargo
│               │   └── config.toml
│               ├── .gitignore
│               ├── Cargo.toml
│               ├── Dockerfile
│               ├── README.md
│               ├── rstime.wasm
│               └── src
│                   ├── lib.rs
│                   └── pdk
│                       ├── exports.rs
│                       ├── imports.rs
│                       ├── mod.rs
│                       └── types.rs
├── iac
│   ├── .terraform.lock.hcl
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── justfile
├── LICENSE
├── README.md
├── RUNTIME_CONFIG.md
├── rust-toolchain.toml
├── server.json
├── SKIP_TOOLS_GUIDE.md
├── src
│   ├── cli.rs
│   ├── config.rs
│   ├── https_auth.rs
│   ├── logging.rs
│   ├── main.rs
│   ├── naming.rs
│   ├── plugin.rs
│   ├── service.rs
│   └── wasm
│       ├── http.rs
│       ├── mod.rs
│       ├── oci.rs
│       └── s3.rs
├── templates
│   └── plugins
│       ├── go
│       │   ├── .gitignore
│       │   ├── Dockerfile
│       │   ├── exports.go
│       │   ├── go.mod
│       │   ├── go.sum
│       │   ├── imports.go
│       │   ├── main.go
│       │   ├── README.md
│       │   └── types.go
│       ├── README.md
│       └── rust
│           ├── .cargo
│           │   └── config.toml
│           ├── .gitignore
│           ├── Cargo.toml
│           ├── Dockerfile
│           ├── README.md
│           └── src
│               ├── lib.rs
│               └── pdk
│                   ├── exports.rs
│                   ├── imports.rs
│                   ├── mod.rs
│                   └── types.rs
├── tests
│   └── fixtures
│       ├── config_with_auths.json
│       ├── config_with_auths.yaml
│       ├── documentation_example.json
│       ├── documentation_example.yaml
│       ├── invalid_auth_config.yaml
│       ├── invalid_plugin_name.yaml
│       ├── invalid_structure.yaml
│       ├── invalid_url.yaml
│       ├── keyring_auth_config.yaml
│       ├── skip_tools_examples.yaml
│       ├── unsupported_config.txt
│       ├── valid_config.json
│       └── valid_config.yaml
└── xtp-plugin-schema.json
```

# Files

--------------------------------------------------------------------------------
/examples/plugins/v1/myip/src/pdk.rs:
--------------------------------------------------------------------------------

```rust
  1 | #![allow(non_snake_case)]
  2 | #![allow(unused_macros)]
  3 | use extism_pdk::*;
  4 | 
  5 | #[allow(unused)]
  6 | fn panic_if_key_missing() -> ! {
  7 |     panic!("missing key");
  8 | }
  9 | 
 10 | pub(crate) mod internal {
 11 |     pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
 12 |         let err = format!("{:?}", e);
 13 |         let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
 14 |         unsafe {
 15 |             extism_pdk::extism::error_set(mem.offset());
 16 |         }
 17 |         -1
 18 |     }
 19 | }
 20 | 
 21 | #[allow(unused)]
 22 | macro_rules! try_input {
 23 |     () => {{
 24 |         let x = extism_pdk::input();
 25 |         match x {
 26 |             Ok(x) => x,
 27 |             Err(e) => return internal::return_error(e),
 28 |         }
 29 |     }};
 30 | }
 31 | 
 32 | #[allow(unused)]
 33 | macro_rules! try_input_json {
 34 |     () => {{
 35 |         let x = extism_pdk::input();
 36 |         match x {
 37 |             Ok(extism_pdk::Json(x)) => x,
 38 |             Err(e) => return internal::return_error(e),
 39 |         }
 40 |     }};
 41 | }
 42 | 
 43 | use base64_serde::base64_serde_type;
 44 | 
 45 | base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
 46 | 
 47 | mod exports {
 48 |     use super::*;
 49 | 
 50 |     #[unsafe(no_mangle)]
 51 |     pub extern "C" fn call() -> i32 {
 52 |         let ret =
 53 |             crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 54 | 
 55 |         match ret {
 56 |             Ok(()) => 0,
 57 |             Err(e) => internal::return_error(e),
 58 |         }
 59 |     }
 60 | 
 61 |     #[unsafe(no_mangle)]
 62 |     pub extern "C" fn describe() -> i32 {
 63 |         let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 64 | 
 65 |         match ret {
 66 |             Ok(()) => 0,
 67 |             Err(e) => internal::return_error(e),
 68 |         }
 69 |     }
 70 | }
 71 | 
 72 | pub mod types {
 73 |     use super::*;
 74 | 
 75 |     #[derive(
 76 |         Default,
 77 |         Debug,
 78 |         Clone,
 79 |         serde::Serialize,
 80 |         serde::Deserialize,
 81 |         extism_pdk::FromBytes,
 82 |         extism_pdk::ToBytes,
 83 |     )]
 84 |     #[encoding(Json)]
 85 |     pub struct BlobResourceContents {
 86 |         /// A base64-encoded string representing the binary data of the item.
 87 |         #[serde(rename = "blob")]
 88 |         pub blob: String,
 89 | 
 90 |         /// The MIME type of this resource, if known.
 91 |         #[serde(rename = "mimeType")]
 92 |         #[serde(skip_serializing_if = "Option::is_none")]
 93 |         #[serde(default)]
 94 |         pub mime_type: Option<String>,
 95 | 
 96 |         /// The URI of this resource.
 97 |         #[serde(rename = "uri")]
 98 |         pub uri: String,
 99 |     }
100 | 
101 |     #[derive(
102 |         Default,
103 |         Debug,
104 |         Clone,
105 |         serde::Serialize,
106 |         serde::Deserialize,
107 |         extism_pdk::FromBytes,
108 |         extism_pdk::ToBytes,
109 |     )]
110 |     #[encoding(Json)]
111 |     pub struct CallToolRequest {
112 |         #[serde(rename = "method")]
113 |         #[serde(skip_serializing_if = "Option::is_none")]
114 |         #[serde(default)]
115 |         pub method: Option<String>,
116 | 
117 |         #[serde(rename = "params")]
118 |         pub params: types::Params,
119 |     }
120 | 
121 |     #[derive(
122 |         Default,
123 |         Debug,
124 |         Clone,
125 |         serde::Serialize,
126 |         serde::Deserialize,
127 |         extism_pdk::FromBytes,
128 |         extism_pdk::ToBytes,
129 |     )]
130 |     #[encoding(Json)]
131 |     pub struct CallToolResult {
132 |         #[serde(rename = "content")]
133 |         pub content: Vec<types::Content>,
134 | 
135 |         /// Whether the tool call ended in an error.
136 |         ///
137 |         /// If not set, this is assumed to be false (the call was successful).
138 |         #[serde(rename = "isError")]
139 |         #[serde(skip_serializing_if = "Option::is_none")]
140 |         #[serde(default)]
141 |         pub is_error: Option<bool>,
142 |     }
143 | 
144 |     #[derive(
145 |         Default,
146 |         Debug,
147 |         Clone,
148 |         serde::Serialize,
149 |         serde::Deserialize,
150 |         extism_pdk::FromBytes,
151 |         extism_pdk::ToBytes,
152 |     )]
153 |     #[encoding(Json)]
154 |     pub struct Content {
155 |         #[serde(rename = "annotations")]
156 |         #[serde(skip_serializing_if = "Option::is_none")]
157 |         #[serde(default)]
158 |         pub annotations: Option<types::TextAnnotation>,
159 | 
160 |         /// The base64-encoded image data.
161 |         #[serde(rename = "data")]
162 |         #[serde(skip_serializing_if = "Option::is_none")]
163 |         #[serde(default)]
164 |         pub data: Option<String>,
165 | 
166 |         /// The MIME type of the image. Different providers may support different image types.
167 |         #[serde(rename = "mimeType")]
168 |         #[serde(skip_serializing_if = "Option::is_none")]
169 |         #[serde(default)]
170 |         pub mime_type: Option<String>,
171 | 
172 |         /// The text content of the message.
173 |         #[serde(rename = "text")]
174 |         #[serde(skip_serializing_if = "Option::is_none")]
175 |         #[serde(default)]
176 |         pub text: Option<String>,
177 | 
178 |         #[serde(rename = "type")]
179 |         pub r#type: types::ContentType,
180 |     }
181 | 
182 |     #[derive(
183 |         Default,
184 |         Debug,
185 |         Clone,
186 |         serde::Serialize,
187 |         serde::Deserialize,
188 |         extism_pdk::FromBytes,
189 |         extism_pdk::ToBytes,
190 |     )]
191 |     #[encoding(Json)]
192 |     pub enum ContentType {
193 |         #[default]
194 |         #[serde(rename = "text")]
195 |         Text,
196 |         #[serde(rename = "image")]
197 |         Image,
198 |         #[serde(rename = "resource")]
199 |         Resource,
200 |     }
201 | 
202 |     #[derive(
203 |         Default,
204 |         Debug,
205 |         Clone,
206 |         serde::Serialize,
207 |         serde::Deserialize,
208 |         extism_pdk::FromBytes,
209 |         extism_pdk::ToBytes,
210 |     )]
211 |     #[encoding(Json)]
212 |     pub struct ListToolsResult {
213 |         /// The list of ToolDescription objects provided by this servlet.
214 |         #[serde(rename = "tools")]
215 |         pub tools: Vec<types::ToolDescription>,
216 |     }
217 | 
218 |     #[derive(
219 |         Default,
220 |         Debug,
221 |         Clone,
222 |         serde::Serialize,
223 |         serde::Deserialize,
224 |         extism_pdk::FromBytes,
225 |         extism_pdk::ToBytes,
226 |     )]
227 |     #[encoding(Json)]
228 |     pub struct Params {
229 |         #[serde(rename = "arguments")]
230 |         #[serde(skip_serializing_if = "Option::is_none")]
231 |         #[serde(default)]
232 |         pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
233 | 
234 |         #[serde(rename = "name")]
235 |         pub name: String,
236 |     }
237 | 
238 |     #[derive(
239 |         Default,
240 |         Debug,
241 |         Clone,
242 |         serde::Serialize,
243 |         serde::Deserialize,
244 |         extism_pdk::FromBytes,
245 |         extism_pdk::ToBytes,
246 |     )]
247 |     #[encoding(Json)]
248 |     pub enum Role {
249 |         #[default]
250 |         #[serde(rename = "assistant")]
251 |         Assistant,
252 |         #[serde(rename = "user")]
253 |         User,
254 |     }
255 | 
256 |     #[derive(
257 |         Default,
258 |         Debug,
259 |         Clone,
260 |         serde::Serialize,
261 |         serde::Deserialize,
262 |         extism_pdk::FromBytes,
263 |         extism_pdk::ToBytes,
264 |     )]
265 |     #[encoding(Json)]
266 |     pub struct TextAnnotation {
267 |         /// Describes who the intended customer of this object or data is.
268 |         ///
269 |         /// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
270 |         #[serde(rename = "audience")]
271 |         pub audience: Vec<types::Role>,
272 | 
273 |         /// Describes how important this data is for operating the server.
274 |         ///
275 |         /// A value of 1 means "most important," and indicates that the data is
276 |         /// effectively required, while 0 means "least important," and indicates that
277 |         /// the data is entirely optional.
278 |         #[serde(rename = "priority")]
279 |         pub priority: f32,
280 |     }
281 | 
282 |     #[derive(
283 |         Default,
284 |         Debug,
285 |         Clone,
286 |         serde::Serialize,
287 |         serde::Deserialize,
288 |         extism_pdk::FromBytes,
289 |         extism_pdk::ToBytes,
290 |     )]
291 |     #[encoding(Json)]
292 |     pub struct TextResourceContents {
293 |         /// The MIME type of this resource, if known.
294 |         #[serde(rename = "mimeType")]
295 |         #[serde(skip_serializing_if = "Option::is_none")]
296 |         #[serde(default)]
297 |         pub mime_type: Option<String>,
298 | 
299 |         /// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
300 |         #[serde(rename = "text")]
301 |         pub text: String,
302 | 
303 |         /// The URI of this resource.
304 |         #[serde(rename = "uri")]
305 |         pub uri: String,
306 |     }
307 | 
308 |     #[derive(
309 |         Default,
310 |         Debug,
311 |         Clone,
312 |         serde::Serialize,
313 |         serde::Deserialize,
314 |         extism_pdk::FromBytes,
315 |         extism_pdk::ToBytes,
316 |     )]
317 |     #[encoding(Json)]
318 |     pub struct ToolDescription {
319 |         /// A description of the tool
320 |         #[serde(rename = "description")]
321 |         pub description: String,
322 | 
323 |         /// The JSON schema describing the argument input
324 |         #[serde(rename = "inputSchema")]
325 |         pub input_schema: serde_json::Map<String, serde_json::Value>,
326 | 
327 |         /// The name of the tool. It should match the plugin / binding name.
328 |         #[serde(rename = "name")]
329 |         pub name: String,
330 |     }
331 | }
332 | 
333 | mod raw_imports {
334 |     use super::*;
335 |     #[host_fn]
336 |     extern "ExtismHost" {}
337 | }
338 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/qdrant/src/pdk.rs:
--------------------------------------------------------------------------------

```rust
  1 | #![allow(non_snake_case)]
  2 | #![allow(unused_macros)]
  3 | use extism_pdk::*;
  4 | 
  5 | #[allow(unused)]
  6 | fn panic_if_key_missing() -> ! {
  7 |     panic!("missing key");
  8 | }
  9 | 
 10 | pub(crate) mod internal {
 11 |     pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
 12 |         let err = format!("{:?}", e);
 13 |         let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
 14 |         unsafe {
 15 |             extism_pdk::extism::error_set(mem.offset());
 16 |         }
 17 |         -1
 18 |     }
 19 | }
 20 | 
 21 | #[allow(unused)]
 22 | macro_rules! try_input {
 23 |     () => {{
 24 |         let x = extism_pdk::input();
 25 |         match x {
 26 |             Ok(x) => x,
 27 |             Err(e) => return internal::return_error(e),
 28 |         }
 29 |     }};
 30 | }
 31 | 
 32 | #[allow(unused)]
 33 | macro_rules! try_input_json {
 34 |     () => {{
 35 |         let x = extism_pdk::input();
 36 |         match x {
 37 |             Ok(extism_pdk::Json(x)) => x,
 38 |             Err(e) => return internal::return_error(e),
 39 |         }
 40 |     }};
 41 | }
 42 | 
 43 | use base64_serde::base64_serde_type;
 44 | 
 45 | base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
 46 | 
 47 | mod exports {
 48 |     use super::*;
 49 | 
 50 |     #[unsafe(no_mangle)]
 51 |     pub extern "C" fn call() -> i32 {
 52 |         let ret =
 53 |             crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 54 | 
 55 |         match ret {
 56 |             Ok(()) => 0,
 57 |             Err(e) => internal::return_error(e),
 58 |         }
 59 |     }
 60 | 
 61 |     #[unsafe(no_mangle)]
 62 |     pub extern "C" fn describe() -> i32 {
 63 |         let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 64 | 
 65 |         match ret {
 66 |             Ok(()) => 0,
 67 |             Err(e) => internal::return_error(e),
 68 |         }
 69 |     }
 70 | }
 71 | 
 72 | pub mod types {
 73 |     use super::*;
 74 | 
 75 |     #[derive(
 76 |         Default,
 77 |         Debug,
 78 |         Clone,
 79 |         serde::Serialize,
 80 |         serde::Deserialize,
 81 |         extism_pdk::FromBytes,
 82 |         extism_pdk::ToBytes,
 83 |     )]
 84 |     #[encoding(Json)]
 85 |     pub struct BlobResourceContents {
 86 |         /// A base64-encoded string representing the binary data of the item.
 87 |         #[serde(rename = "blob")]
 88 |         pub blob: String,
 89 | 
 90 |         /// The MIME type of this resource, if known.
 91 |         #[serde(rename = "mimeType")]
 92 |         #[serde(skip_serializing_if = "Option::is_none")]
 93 |         #[serde(default)]
 94 |         pub mime_type: Option<String>,
 95 | 
 96 |         /// The URI of this resource.
 97 |         #[serde(rename = "uri")]
 98 |         pub uri: String,
 99 |     }
100 | 
101 |     #[derive(
102 |         Default,
103 |         Debug,
104 |         Clone,
105 |         serde::Serialize,
106 |         serde::Deserialize,
107 |         extism_pdk::FromBytes,
108 |         extism_pdk::ToBytes,
109 |     )]
110 |     #[encoding(Json)]
111 |     pub struct CallToolRequest {
112 |         #[serde(rename = "method")]
113 |         #[serde(skip_serializing_if = "Option::is_none")]
114 |         #[serde(default)]
115 |         pub method: Option<String>,
116 | 
117 |         #[serde(rename = "params")]
118 |         pub params: types::Params,
119 |     }
120 | 
121 |     #[derive(
122 |         Default,
123 |         Debug,
124 |         Clone,
125 |         serde::Serialize,
126 |         serde::Deserialize,
127 |         extism_pdk::FromBytes,
128 |         extism_pdk::ToBytes,
129 |     )]
130 |     #[encoding(Json)]
131 |     pub struct CallToolResult {
132 |         #[serde(rename = "content")]
133 |         pub content: Vec<types::Content>,
134 | 
135 |         /// Whether the tool call ended in an error.
136 |         ///
137 |         /// If not set, this is assumed to be false (the call was successful).
138 |         #[serde(rename = "isError")]
139 |         #[serde(skip_serializing_if = "Option::is_none")]
140 |         #[serde(default)]
141 |         pub is_error: Option<bool>,
142 |     }
143 | 
144 |     #[derive(
145 |         Default,
146 |         Debug,
147 |         Clone,
148 |         serde::Serialize,
149 |         serde::Deserialize,
150 |         extism_pdk::FromBytes,
151 |         extism_pdk::ToBytes,
152 |     )]
153 |     #[encoding(Json)]
154 |     pub struct Content {
155 |         #[serde(rename = "annotations")]
156 |         #[serde(skip_serializing_if = "Option::is_none")]
157 |         #[serde(default)]
158 |         pub annotations: Option<types::TextAnnotation>,
159 | 
160 |         /// The base64-encoded image data.
161 |         #[serde(rename = "data")]
162 |         #[serde(skip_serializing_if = "Option::is_none")]
163 |         #[serde(default)]
164 |         pub data: Option<String>,
165 | 
166 |         /// The MIME type of the image. Different providers may support different image types.
167 |         #[serde(rename = "mimeType")]
168 |         #[serde(skip_serializing_if = "Option::is_none")]
169 |         #[serde(default)]
170 |         pub mime_type: Option<String>,
171 | 
172 |         /// The text content of the message.
173 |         #[serde(rename = "text")]
174 |         #[serde(skip_serializing_if = "Option::is_none")]
175 |         #[serde(default)]
176 |         pub text: Option<String>,
177 | 
178 |         #[serde(rename = "type")]
179 |         pub r#type: types::ContentType,
180 |     }
181 | 
182 |     #[derive(
183 |         Default,
184 |         Debug,
185 |         Clone,
186 |         serde::Serialize,
187 |         serde::Deserialize,
188 |         extism_pdk::FromBytes,
189 |         extism_pdk::ToBytes,
190 |     )]
191 |     #[encoding(Json)]
192 |     pub enum ContentType {
193 |         #[default]
194 |         #[serde(rename = "text")]
195 |         Text,
196 |         #[serde(rename = "image")]
197 |         Image,
198 |         #[serde(rename = "resource")]
199 |         Resource,
200 |     }
201 | 
202 |     #[derive(
203 |         Default,
204 |         Debug,
205 |         Clone,
206 |         serde::Serialize,
207 |         serde::Deserialize,
208 |         extism_pdk::FromBytes,
209 |         extism_pdk::ToBytes,
210 |     )]
211 |     #[encoding(Json)]
212 |     pub struct ListToolsResult {
213 |         /// The list of ToolDescription objects provided by this servlet.
214 |         #[serde(rename = "tools")]
215 |         pub tools: Vec<types::ToolDescription>,
216 |     }
217 | 
218 |     #[derive(
219 |         Default,
220 |         Debug,
221 |         Clone,
222 |         serde::Serialize,
223 |         serde::Deserialize,
224 |         extism_pdk::FromBytes,
225 |         extism_pdk::ToBytes,
226 |     )]
227 |     #[encoding(Json)]
228 |     pub struct Params {
229 |         #[serde(rename = "arguments")]
230 |         #[serde(skip_serializing_if = "Option::is_none")]
231 |         #[serde(default)]
232 |         pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
233 | 
234 |         #[serde(rename = "name")]
235 |         pub name: String,
236 |     }
237 | 
238 |     #[derive(
239 |         Default,
240 |         Debug,
241 |         Clone,
242 |         serde::Serialize,
243 |         serde::Deserialize,
244 |         extism_pdk::FromBytes,
245 |         extism_pdk::ToBytes,
246 |     )]
247 |     #[encoding(Json)]
248 |     pub enum Role {
249 |         #[default]
250 |         #[serde(rename = "assistant")]
251 |         Assistant,
252 |         #[serde(rename = "user")]
253 |         User,
254 |     }
255 | 
256 |     #[derive(
257 |         Default,
258 |         Debug,
259 |         Clone,
260 |         serde::Serialize,
261 |         serde::Deserialize,
262 |         extism_pdk::FromBytes,
263 |         extism_pdk::ToBytes,
264 |     )]
265 |     #[encoding(Json)]
266 |     pub struct TextAnnotation {
267 |         /// Describes who the intended customer of this object or data is.
268 |         ///
269 |         /// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
270 |         #[serde(rename = "audience")]
271 |         pub audience: Vec<types::Role>,
272 | 
273 |         /// Describes how important this data is for operating the server.
274 |         ///
275 |         /// A value of 1 means "most important," and indicates that the data is
276 |         /// effectively required, while 0 means "least important," and indicates that
277 |         /// the data is entirely optional.
278 |         #[serde(rename = "priority")]
279 |         pub priority: f32,
280 |     }
281 | 
282 |     #[derive(
283 |         Default,
284 |         Debug,
285 |         Clone,
286 |         serde::Serialize,
287 |         serde::Deserialize,
288 |         extism_pdk::FromBytes,
289 |         extism_pdk::ToBytes,
290 |     )]
291 |     #[encoding(Json)]
292 |     pub struct TextResourceContents {
293 |         /// The MIME type of this resource, if known.
294 |         #[serde(rename = "mimeType")]
295 |         #[serde(skip_serializing_if = "Option::is_none")]
296 |         #[serde(default)]
297 |         pub mime_type: Option<String>,
298 | 
299 |         /// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
300 |         #[serde(rename = "text")]
301 |         pub text: String,
302 | 
303 |         /// The URI of this resource.
304 |         #[serde(rename = "uri")]
305 |         pub uri: String,
306 |     }
307 | 
308 |     #[derive(
309 |         Default,
310 |         Debug,
311 |         Clone,
312 |         serde::Serialize,
313 |         serde::Deserialize,
314 |         extism_pdk::FromBytes,
315 |         extism_pdk::ToBytes,
316 |     )]
317 |     #[encoding(Json)]
318 |     pub struct ToolDescription {
319 |         /// A description of the tool
320 |         #[serde(rename = "description")]
321 |         pub description: String,
322 | 
323 |         /// The JSON schema describing the argument input
324 |         #[serde(rename = "inputSchema")]
325 |         pub input_schema: serde_json::Map<String, serde_json::Value>,
326 | 
327 |         /// The name of the tool. It should match the plugin / binding name.
328 |         #[serde(rename = "name")]
329 |         pub name: String,
330 |     }
331 | }
332 | 
333 | mod raw_imports {
334 |     use super::*;
335 |     #[host_fn]
336 |     extern "ExtismHost" {}
337 | }
338 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/serper/src/pdk.rs:
--------------------------------------------------------------------------------

```rust
  1 | #![allow(non_snake_case)]
  2 | #![allow(unused_macros)]
  3 | use extism_pdk::*;
  4 | 
  5 | #[allow(unused)]
  6 | fn panic_if_key_missing() -> ! {
  7 |     panic!("missing key");
  8 | }
  9 | 
 10 | pub(crate) mod internal {
 11 |     pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
 12 |         let err = format!("{:?}", e);
 13 |         let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
 14 |         unsafe {
 15 |             extism_pdk::extism::error_set(mem.offset());
 16 |         }
 17 |         -1
 18 |     }
 19 | }
 20 | 
 21 | #[allow(unused)]
 22 | macro_rules! try_input {
 23 |     () => {{
 24 |         let x = extism_pdk::input();
 25 |         match x {
 26 |             Ok(x) => x,
 27 |             Err(e) => return internal::return_error(e),
 28 |         }
 29 |     }};
 30 | }
 31 | 
 32 | #[allow(unused)]
 33 | macro_rules! try_input_json {
 34 |     () => {{
 35 |         let x = extism_pdk::input();
 36 |         match x {
 37 |             Ok(extism_pdk::Json(x)) => x,
 38 |             Err(e) => return internal::return_error(e),
 39 |         }
 40 |     }};
 41 | }
 42 | 
 43 | use base64_serde::base64_serde_type;
 44 | 
 45 | base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
 46 | 
 47 | mod exports {
 48 |     use super::*;
 49 | 
 50 |     #[unsafe(no_mangle)]
 51 |     pub extern "C" fn call() -> i32 {
 52 |         let ret =
 53 |             crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 54 | 
 55 |         match ret {
 56 |             Ok(()) => 0,
 57 |             Err(e) => internal::return_error(e),
 58 |         }
 59 |     }
 60 | 
 61 |     #[unsafe(no_mangle)]
 62 |     pub extern "C" fn describe() -> i32 {
 63 |         let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 64 | 
 65 |         match ret {
 66 |             Ok(()) => 0,
 67 |             Err(e) => internal::return_error(e),
 68 |         }
 69 |     }
 70 | }
 71 | 
 72 | pub mod types {
 73 |     use super::*;
 74 | 
 75 |     #[derive(
 76 |         Default,
 77 |         Debug,
 78 |         Clone,
 79 |         serde::Serialize,
 80 |         serde::Deserialize,
 81 |         extism_pdk::FromBytes,
 82 |         extism_pdk::ToBytes,
 83 |     )]
 84 |     #[encoding(Json)]
 85 |     pub struct BlobResourceContents {
 86 |         /// A base64-encoded string representing the binary data of the item.
 87 |         #[serde(rename = "blob")]
 88 |         pub blob: String,
 89 | 
 90 |         /// The MIME type of this resource, if known.
 91 |         #[serde(rename = "mimeType")]
 92 |         #[serde(skip_serializing_if = "Option::is_none")]
 93 |         #[serde(default)]
 94 |         pub mime_type: Option<String>,
 95 | 
 96 |         /// The URI of this resource.
 97 |         #[serde(rename = "uri")]
 98 |         pub uri: String,
 99 |     }
100 | 
101 |     #[derive(
102 |         Default,
103 |         Debug,
104 |         Clone,
105 |         serde::Serialize,
106 |         serde::Deserialize,
107 |         extism_pdk::FromBytes,
108 |         extism_pdk::ToBytes,
109 |     )]
110 |     #[encoding(Json)]
111 |     pub struct CallToolRequest {
112 |         #[serde(rename = "method")]
113 |         #[serde(skip_serializing_if = "Option::is_none")]
114 |         #[serde(default)]
115 |         pub method: Option<String>,
116 | 
117 |         #[serde(rename = "params")]
118 |         pub params: types::Params,
119 |     }
120 | 
121 |     #[derive(
122 |         Default,
123 |         Debug,
124 |         Clone,
125 |         serde::Serialize,
126 |         serde::Deserialize,
127 |         extism_pdk::FromBytes,
128 |         extism_pdk::ToBytes,
129 |     )]
130 |     #[encoding(Json)]
131 |     pub struct CallToolResult {
132 |         #[serde(rename = "content")]
133 |         pub content: Vec<types::Content>,
134 | 
135 |         /// Whether the tool call ended in an error.
136 |         ///
137 |         /// If not set, this is assumed to be false (the call was successful).
138 |         #[serde(rename = "isError")]
139 |         #[serde(skip_serializing_if = "Option::is_none")]
140 |         #[serde(default)]
141 |         pub is_error: Option<bool>,
142 |     }
143 | 
144 |     #[derive(
145 |         Default,
146 |         Debug,
147 |         Clone,
148 |         serde::Serialize,
149 |         serde::Deserialize,
150 |         extism_pdk::FromBytes,
151 |         extism_pdk::ToBytes,
152 |     )]
153 |     #[encoding(Json)]
154 |     pub struct Content {
155 |         #[serde(rename = "annotations")]
156 |         #[serde(skip_serializing_if = "Option::is_none")]
157 |         #[serde(default)]
158 |         pub annotations: Option<types::TextAnnotation>,
159 | 
160 |         /// The base64-encoded image data.
161 |         #[serde(rename = "data")]
162 |         #[serde(skip_serializing_if = "Option::is_none")]
163 |         #[serde(default)]
164 |         pub data: Option<String>,
165 | 
166 |         /// The MIME type of the image. Different providers may support different image types.
167 |         #[serde(rename = "mimeType")]
168 |         #[serde(skip_serializing_if = "Option::is_none")]
169 |         #[serde(default)]
170 |         pub mime_type: Option<String>,
171 | 
172 |         /// The text content of the message.
173 |         #[serde(rename = "text")]
174 |         #[serde(skip_serializing_if = "Option::is_none")]
175 |         #[serde(default)]
176 |         pub text: Option<String>,
177 | 
178 |         #[serde(rename = "type")]
179 |         pub r#type: types::ContentType,
180 |     }
181 | 
182 |     #[derive(
183 |         Default,
184 |         Debug,
185 |         Clone,
186 |         serde::Serialize,
187 |         serde::Deserialize,
188 |         extism_pdk::FromBytes,
189 |         extism_pdk::ToBytes,
190 |     )]
191 |     #[encoding(Json)]
192 |     pub enum ContentType {
193 |         #[default]
194 |         #[serde(rename = "text")]
195 |         Text,
196 |         #[serde(rename = "image")]
197 |         Image,
198 |         #[serde(rename = "resource")]
199 |         Resource,
200 |     }
201 | 
202 |     #[derive(
203 |         Default,
204 |         Debug,
205 |         Clone,
206 |         serde::Serialize,
207 |         serde::Deserialize,
208 |         extism_pdk::FromBytes,
209 |         extism_pdk::ToBytes,
210 |     )]
211 |     #[encoding(Json)]
212 |     pub struct ListToolsResult {
213 |         /// The list of ToolDescription objects provided by this servlet.
214 |         #[serde(rename = "tools")]
215 |         pub tools: Vec<types::ToolDescription>,
216 |     }
217 | 
218 |     #[derive(
219 |         Default,
220 |         Debug,
221 |         Clone,
222 |         serde::Serialize,
223 |         serde::Deserialize,
224 |         extism_pdk::FromBytes,
225 |         extism_pdk::ToBytes,
226 |     )]
227 |     #[encoding(Json)]
228 |     pub struct Params {
229 |         #[serde(rename = "arguments")]
230 |         #[serde(skip_serializing_if = "Option::is_none")]
231 |         #[serde(default)]
232 |         pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
233 | 
234 |         #[serde(rename = "name")]
235 |         pub name: String,
236 |     }
237 | 
238 |     #[derive(
239 |         Default,
240 |         Debug,
241 |         Clone,
242 |         serde::Serialize,
243 |         serde::Deserialize,
244 |         extism_pdk::FromBytes,
245 |         extism_pdk::ToBytes,
246 |     )]
247 |     #[encoding(Json)]
248 |     pub enum Role {
249 |         #[default]
250 |         #[serde(rename = "assistant")]
251 |         Assistant,
252 |         #[serde(rename = "user")]
253 |         User,
254 |     }
255 | 
256 |     #[derive(
257 |         Default,
258 |         Debug,
259 |         Clone,
260 |         serde::Serialize,
261 |         serde::Deserialize,
262 |         extism_pdk::FromBytes,
263 |         extism_pdk::ToBytes,
264 |     )]
265 |     #[encoding(Json)]
266 |     pub struct TextAnnotation {
267 |         /// Describes who the intended customer of this object or data is.
268 |         ///
269 |         /// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
270 |         #[serde(rename = "audience")]
271 |         pub audience: Vec<types::Role>,
272 | 
273 |         /// Describes how important this data is for operating the server.
274 |         ///
275 |         /// A value of 1 means "most important," and indicates that the data is
276 |         /// effectively required, while 0 means "least important," and indicates that
277 |         /// the data is entirely optional.
278 |         #[serde(rename = "priority")]
279 |         pub priority: f32,
280 |     }
281 | 
282 |     #[derive(
283 |         Default,
284 |         Debug,
285 |         Clone,
286 |         serde::Serialize,
287 |         serde::Deserialize,
288 |         extism_pdk::FromBytes,
289 |         extism_pdk::ToBytes,
290 |     )]
291 |     #[encoding(Json)]
292 |     pub struct TextResourceContents {
293 |         /// The MIME type of this resource, if known.
294 |         #[serde(rename = "mimeType")]
295 |         #[serde(skip_serializing_if = "Option::is_none")]
296 |         #[serde(default)]
297 |         pub mime_type: Option<String>,
298 | 
299 |         /// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
300 |         #[serde(rename = "text")]
301 |         pub text: String,
302 | 
303 |         /// The URI of this resource.
304 |         #[serde(rename = "uri")]
305 |         pub uri: String,
306 |     }
307 | 
308 |     #[derive(
309 |         Default,
310 |         Debug,
311 |         Clone,
312 |         serde::Serialize,
313 |         serde::Deserialize,
314 |         extism_pdk::FromBytes,
315 |         extism_pdk::ToBytes,
316 |     )]
317 |     #[encoding(Json)]
318 |     pub struct ToolDescription {
319 |         /// A description of the tool
320 |         #[serde(rename = "description")]
321 |         pub description: String,
322 | 
323 |         /// The JSON schema describing the argument input
324 |         #[serde(rename = "inputSchema")]
325 |         pub input_schema: serde_json::Map<String, serde_json::Value>,
326 | 
327 |         /// The name of the tool. It should match the plugin / binding name.
328 |         #[serde(rename = "name")]
329 |         pub name: String,
330 |     }
331 | }
332 | 
333 | mod raw_imports {
334 |     use super::*;
335 |     #[host_fn]
336 |     extern "ExtismHost" {}
337 | }
338 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/sqlite/src/pdk.rs:
--------------------------------------------------------------------------------

```rust
  1 | #![allow(non_snake_case)]
  2 | #![allow(unused_macros)]
  3 | use extism_pdk::*;
  4 | 
  5 | #[allow(unused)]
  6 | fn panic_if_key_missing() -> ! {
  7 |     panic!("missing key");
  8 | }
  9 | 
 10 | pub(crate) mod internal {
 11 |     pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
 12 |         let err = format!("{:?}", e);
 13 |         let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
 14 |         unsafe {
 15 |             extism_pdk::extism::error_set(mem.offset());
 16 |         }
 17 |         -1
 18 |     }
 19 | }
 20 | 
 21 | #[allow(unused)]
 22 | macro_rules! try_input {
 23 |     () => {{
 24 |         let x = extism_pdk::input();
 25 |         match x {
 26 |             Ok(x) => x,
 27 |             Err(e) => return internal::return_error(e),
 28 |         }
 29 |     }};
 30 | }
 31 | 
 32 | #[allow(unused)]
 33 | macro_rules! try_input_json {
 34 |     () => {{
 35 |         let x = extism_pdk::input();
 36 |         match x {
 37 |             Ok(extism_pdk::Json(x)) => x,
 38 |             Err(e) => return internal::return_error(e),
 39 |         }
 40 |     }};
 41 | }
 42 | 
 43 | use base64_serde::base64_serde_type;
 44 | 
 45 | base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
 46 | 
 47 | mod exports {
 48 |     use super::*;
 49 | 
 50 |     #[unsafe(no_mangle)]
 51 |     pub extern "C" fn call() -> i32 {
 52 |         let ret =
 53 |             crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 54 | 
 55 |         match ret {
 56 |             Ok(()) => 0,
 57 |             Err(e) => internal::return_error(e),
 58 |         }
 59 |     }
 60 | 
 61 |     #[unsafe(no_mangle)]
 62 |     pub extern "C" fn describe() -> i32 {
 63 |         let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 64 | 
 65 |         match ret {
 66 |             Ok(()) => 0,
 67 |             Err(e) => internal::return_error(e),
 68 |         }
 69 |     }
 70 | }
 71 | 
 72 | pub mod types {
 73 |     use super::*;
 74 | 
 75 |     #[derive(
 76 |         Default,
 77 |         Debug,
 78 |         Clone,
 79 |         serde::Serialize,
 80 |         serde::Deserialize,
 81 |         extism_pdk::FromBytes,
 82 |         extism_pdk::ToBytes,
 83 |     )]
 84 |     #[encoding(Json)]
 85 |     pub struct BlobResourceContents {
 86 |         /// A base64-encoded string representing the binary data of the item.
 87 |         #[serde(rename = "blob")]
 88 |         pub blob: String,
 89 | 
 90 |         /// The MIME type of this resource, if known.
 91 |         #[serde(rename = "mimeType")]
 92 |         #[serde(skip_serializing_if = "Option::is_none")]
 93 |         #[serde(default)]
 94 |         pub mime_type: Option<String>,
 95 | 
 96 |         /// The URI of this resource.
 97 |         #[serde(rename = "uri")]
 98 |         pub uri: String,
 99 |     }
100 | 
101 |     #[derive(
102 |         Default,
103 |         Debug,
104 |         Clone,
105 |         serde::Serialize,
106 |         serde::Deserialize,
107 |         extism_pdk::FromBytes,
108 |         extism_pdk::ToBytes,
109 |     )]
110 |     #[encoding(Json)]
111 |     pub struct CallToolRequest {
112 |         #[serde(rename = "method")]
113 |         #[serde(skip_serializing_if = "Option::is_none")]
114 |         #[serde(default)]
115 |         pub method: Option<String>,
116 | 
117 |         #[serde(rename = "params")]
118 |         pub params: types::Params,
119 |     }
120 | 
121 |     #[derive(
122 |         Default,
123 |         Debug,
124 |         Clone,
125 |         serde::Serialize,
126 |         serde::Deserialize,
127 |         extism_pdk::FromBytes,
128 |         extism_pdk::ToBytes,
129 |     )]
130 |     #[encoding(Json)]
131 |     pub struct CallToolResult {
132 |         #[serde(rename = "content")]
133 |         pub content: Vec<types::Content>,
134 | 
135 |         /// Whether the tool call ended in an error.
136 |         ///
137 |         /// If not set, this is assumed to be false (the call was successful).
138 |         #[serde(rename = "isError")]
139 |         #[serde(skip_serializing_if = "Option::is_none")]
140 |         #[serde(default)]
141 |         pub is_error: Option<bool>,
142 |     }
143 | 
144 |     #[derive(
145 |         Default,
146 |         Debug,
147 |         Clone,
148 |         serde::Serialize,
149 |         serde::Deserialize,
150 |         extism_pdk::FromBytes,
151 |         extism_pdk::ToBytes,
152 |     )]
153 |     #[encoding(Json)]
154 |     pub struct Content {
155 |         #[serde(rename = "annotations")]
156 |         #[serde(skip_serializing_if = "Option::is_none")]
157 |         #[serde(default)]
158 |         pub annotations: Option<types::TextAnnotation>,
159 | 
160 |         /// The base64-encoded image data.
161 |         #[serde(rename = "data")]
162 |         #[serde(skip_serializing_if = "Option::is_none")]
163 |         #[serde(default)]
164 |         pub data: Option<String>,
165 | 
166 |         /// The MIME type of the image. Different providers may support different image types.
167 |         #[serde(rename = "mimeType")]
168 |         #[serde(skip_serializing_if = "Option::is_none")]
169 |         #[serde(default)]
170 |         pub mime_type: Option<String>,
171 | 
172 |         /// The text content of the message.
173 |         #[serde(rename = "text")]
174 |         #[serde(skip_serializing_if = "Option::is_none")]
175 |         #[serde(default)]
176 |         pub text: Option<String>,
177 | 
178 |         #[serde(rename = "type")]
179 |         pub r#type: types::ContentType,
180 |     }
181 | 
182 |     #[derive(
183 |         Default,
184 |         Debug,
185 |         Clone,
186 |         serde::Serialize,
187 |         serde::Deserialize,
188 |         extism_pdk::FromBytes,
189 |         extism_pdk::ToBytes,
190 |     )]
191 |     #[encoding(Json)]
192 |     pub enum ContentType {
193 |         #[default]
194 |         #[serde(rename = "text")]
195 |         Text,
196 |         #[serde(rename = "image")]
197 |         Image,
198 |         #[serde(rename = "resource")]
199 |         Resource,
200 |     }
201 | 
202 |     #[derive(
203 |         Default,
204 |         Debug,
205 |         Clone,
206 |         serde::Serialize,
207 |         serde::Deserialize,
208 |         extism_pdk::FromBytes,
209 |         extism_pdk::ToBytes,
210 |     )]
211 |     #[encoding(Json)]
212 |     pub struct ListToolsResult {
213 |         /// The list of ToolDescription objects provided by this servlet.
214 |         #[serde(rename = "tools")]
215 |         pub tools: Vec<types::ToolDescription>,
216 |     }
217 | 
218 |     #[derive(
219 |         Default,
220 |         Debug,
221 |         Clone,
222 |         serde::Serialize,
223 |         serde::Deserialize,
224 |         extism_pdk::FromBytes,
225 |         extism_pdk::ToBytes,
226 |     )]
227 |     #[encoding(Json)]
228 |     pub struct Params {
229 |         #[serde(rename = "arguments")]
230 |         #[serde(skip_serializing_if = "Option::is_none")]
231 |         #[serde(default)]
232 |         pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
233 | 
234 |         #[serde(rename = "name")]
235 |         pub name: String,
236 |     }
237 | 
238 |     #[derive(
239 |         Default,
240 |         Debug,
241 |         Clone,
242 |         serde::Serialize,
243 |         serde::Deserialize,
244 |         extism_pdk::FromBytes,
245 |         extism_pdk::ToBytes,
246 |     )]
247 |     #[encoding(Json)]
248 |     pub enum Role {
249 |         #[default]
250 |         #[serde(rename = "assistant")]
251 |         Assistant,
252 |         #[serde(rename = "user")]
253 |         User,
254 |     }
255 | 
256 |     #[derive(
257 |         Default,
258 |         Debug,
259 |         Clone,
260 |         serde::Serialize,
261 |         serde::Deserialize,
262 |         extism_pdk::FromBytes,
263 |         extism_pdk::ToBytes,
264 |     )]
265 |     #[encoding(Json)]
266 |     pub struct TextAnnotation {
267 |         /// Describes who the intended customer of this object or data is.
268 |         ///
269 |         /// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
270 |         #[serde(rename = "audience")]
271 |         pub audience: Vec<types::Role>,
272 | 
273 |         /// Describes how important this data is for operating the server.
274 |         ///
275 |         /// A value of 1 means "most important," and indicates that the data is
276 |         /// effectively required, while 0 means "least important," and indicates that
277 |         /// the data is entirely optional.
278 |         #[serde(rename = "priority")]
279 |         pub priority: f32,
280 |     }
281 | 
282 |     #[derive(
283 |         Default,
284 |         Debug,
285 |         Clone,
286 |         serde::Serialize,
287 |         serde::Deserialize,
288 |         extism_pdk::FromBytes,
289 |         extism_pdk::ToBytes,
290 |     )]
291 |     #[encoding(Json)]
292 |     pub struct TextResourceContents {
293 |         /// The MIME type of this resource, if known.
294 |         #[serde(rename = "mimeType")]
295 |         #[serde(skip_serializing_if = "Option::is_none")]
296 |         #[serde(default)]
297 |         pub mime_type: Option<String>,
298 | 
299 |         /// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
300 |         #[serde(rename = "text")]
301 |         pub text: String,
302 | 
303 |         /// The URI of this resource.
304 |         #[serde(rename = "uri")]
305 |         pub uri: String,
306 |     }
307 | 
308 |     #[derive(
309 |         Default,
310 |         Debug,
311 |         Clone,
312 |         serde::Serialize,
313 |         serde::Deserialize,
314 |         extism_pdk::FromBytes,
315 |         extism_pdk::ToBytes,
316 |     )]
317 |     #[encoding(Json)]
318 |     pub struct ToolDescription {
319 |         /// A description of the tool
320 |         #[serde(rename = "description")]
321 |         pub description: String,
322 | 
323 |         /// The JSON schema describing the argument input
324 |         #[serde(rename = "inputSchema")]
325 |         pub input_schema: serde_json::Map<String, serde_json::Value>,
326 | 
327 |         /// The name of the tool. It should match the plugin / binding name.
328 |         #[serde(rename = "name")]
329 |         pub name: String,
330 |     }
331 | }
332 | 
333 | mod raw_imports {
334 |     use super::*;
335 |     #[host_fn]
336 |     extern "ExtismHost" {}
337 | }
338 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/think/src/pdk.rs:
--------------------------------------------------------------------------------

```rust
  1 | #![allow(non_snake_case)]
  2 | #![allow(unused_macros)]
  3 | use extism_pdk::*;
  4 | 
  5 | #[allow(unused)]
  6 | fn panic_if_key_missing() -> ! {
  7 |     panic!("missing key");
  8 | }
  9 | 
 10 | pub(crate) mod internal {
 11 |     pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
 12 |         let err = format!("{:?}", e);
 13 |         let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
 14 |         unsafe {
 15 |             extism_pdk::extism::error_set(mem.offset());
 16 |         }
 17 |         -1
 18 |     }
 19 | }
 20 | 
 21 | #[allow(unused)]
 22 | macro_rules! try_input {
 23 |     () => {{
 24 |         let x = extism_pdk::input();
 25 |         match x {
 26 |             Ok(x) => x,
 27 |             Err(e) => return internal::return_error(e),
 28 |         }
 29 |     }};
 30 | }
 31 | 
 32 | #[allow(unused)]
 33 | macro_rules! try_input_json {
 34 |     () => {{
 35 |         let x = extism_pdk::input();
 36 |         match x {
 37 |             Ok(extism_pdk::Json(x)) => x,
 38 |             Err(e) => return internal::return_error(e),
 39 |         }
 40 |     }};
 41 | }
 42 | 
 43 | use base64_serde::base64_serde_type;
 44 | 
 45 | base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
 46 | 
 47 | mod exports {
 48 |     use super::*;
 49 | 
 50 |     #[unsafe(no_mangle)]
 51 |     pub extern "C" fn call() -> i32 {
 52 |         let ret =
 53 |             crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 54 | 
 55 |         match ret {
 56 |             Ok(()) => 0,
 57 |             Err(e) => internal::return_error(e),
 58 |         }
 59 |     }
 60 | 
 61 |     #[unsafe(no_mangle)]
 62 |     pub extern "C" fn describe() -> i32 {
 63 |         let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 64 | 
 65 |         match ret {
 66 |             Ok(()) => 0,
 67 |             Err(e) => internal::return_error(e),
 68 |         }
 69 |     }
 70 | }
 71 | 
 72 | pub mod types {
 73 |     use super::*;
 74 | 
 75 |     #[derive(
 76 |         Default,
 77 |         Debug,
 78 |         Clone,
 79 |         serde::Serialize,
 80 |         serde::Deserialize,
 81 |         extism_pdk::FromBytes,
 82 |         extism_pdk::ToBytes,
 83 |     )]
 84 |     #[encoding(Json)]
 85 |     pub struct BlobResourceContents {
 86 |         /// A base64-encoded string representing the binary data of the item.
 87 |         #[serde(rename = "blob")]
 88 |         pub blob: String,
 89 | 
 90 |         /// The MIME type of this resource, if known.
 91 |         #[serde(rename = "mimeType")]
 92 |         #[serde(skip_serializing_if = "Option::is_none")]
 93 |         #[serde(default)]
 94 |         pub mime_type: Option<String>,
 95 | 
 96 |         /// The URI of this resource.
 97 |         #[serde(rename = "uri")]
 98 |         pub uri: String,
 99 |     }
100 | 
101 |     #[derive(
102 |         Default,
103 |         Debug,
104 |         Clone,
105 |         serde::Serialize,
106 |         serde::Deserialize,
107 |         extism_pdk::FromBytes,
108 |         extism_pdk::ToBytes,
109 |     )]
110 |     #[encoding(Json)]
111 |     pub struct CallToolRequest {
112 |         #[serde(rename = "method")]
113 |         #[serde(skip_serializing_if = "Option::is_none")]
114 |         #[serde(default)]
115 |         pub method: Option<String>,
116 | 
117 |         #[serde(rename = "params")]
118 |         pub params: types::Params,
119 |     }
120 | 
121 |     #[derive(
122 |         Default,
123 |         Debug,
124 |         Clone,
125 |         serde::Serialize,
126 |         serde::Deserialize,
127 |         extism_pdk::FromBytes,
128 |         extism_pdk::ToBytes,
129 |     )]
130 |     #[encoding(Json)]
131 |     pub struct CallToolResult {
132 |         #[serde(rename = "content")]
133 |         pub content: Vec<types::Content>,
134 | 
135 |         /// Whether the tool call ended in an error.
136 |         ///
137 |         /// If not set, this is assumed to be false (the call was successful).
138 |         #[serde(rename = "isError")]
139 |         #[serde(skip_serializing_if = "Option::is_none")]
140 |         #[serde(default)]
141 |         pub is_error: Option<bool>,
142 |     }
143 | 
144 |     #[derive(
145 |         Default,
146 |         Debug,
147 |         Clone,
148 |         serde::Serialize,
149 |         serde::Deserialize,
150 |         extism_pdk::FromBytes,
151 |         extism_pdk::ToBytes,
152 |     )]
153 |     #[encoding(Json)]
154 |     pub struct Content {
155 |         #[serde(rename = "annotations")]
156 |         #[serde(skip_serializing_if = "Option::is_none")]
157 |         #[serde(default)]
158 |         pub annotations: Option<types::TextAnnotation>,
159 | 
160 |         /// The base64-encoded image data.
161 |         #[serde(rename = "data")]
162 |         #[serde(skip_serializing_if = "Option::is_none")]
163 |         #[serde(default)]
164 |         pub data: Option<String>,
165 | 
166 |         /// The MIME type of the image. Different providers may support different image types.
167 |         #[serde(rename = "mimeType")]
168 |         #[serde(skip_serializing_if = "Option::is_none")]
169 |         #[serde(default)]
170 |         pub mime_type: Option<String>,
171 | 
172 |         /// The text content of the message.
173 |         #[serde(rename = "text")]
174 |         #[serde(skip_serializing_if = "Option::is_none")]
175 |         #[serde(default)]
176 |         pub text: Option<String>,
177 | 
178 |         #[serde(rename = "type")]
179 |         pub r#type: types::ContentType,
180 |     }
181 | 
182 |     #[derive(
183 |         Default,
184 |         Debug,
185 |         Clone,
186 |         serde::Serialize,
187 |         serde::Deserialize,
188 |         extism_pdk::FromBytes,
189 |         extism_pdk::ToBytes,
190 |     )]
191 |     #[encoding(Json)]
192 |     pub enum ContentType {
193 |         #[default]
194 |         #[serde(rename = "text")]
195 |         Text,
196 |         #[serde(rename = "image")]
197 |         Image,
198 |         #[serde(rename = "resource")]
199 |         Resource,
200 |     }
201 | 
202 |     #[derive(
203 |         Default,
204 |         Debug,
205 |         Clone,
206 |         serde::Serialize,
207 |         serde::Deserialize,
208 |         extism_pdk::FromBytes,
209 |         extism_pdk::ToBytes,
210 |     )]
211 |     #[encoding(Json)]
212 |     pub struct ListToolsResult {
213 |         /// The list of ToolDescription objects provided by this servlet.
214 |         #[serde(rename = "tools")]
215 |         pub tools: Vec<types::ToolDescription>,
216 |     }
217 | 
218 |     #[derive(
219 |         Default,
220 |         Debug,
221 |         Clone,
222 |         serde::Serialize,
223 |         serde::Deserialize,
224 |         extism_pdk::FromBytes,
225 |         extism_pdk::ToBytes,
226 |     )]
227 |     #[encoding(Json)]
228 |     pub struct Params {
229 |         #[serde(rename = "arguments")]
230 |         #[serde(skip_serializing_if = "Option::is_none")]
231 |         #[serde(default)]
232 |         pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
233 | 
234 |         #[serde(rename = "name")]
235 |         pub name: String,
236 |     }
237 | 
238 |     #[derive(
239 |         Default,
240 |         Debug,
241 |         Clone,
242 |         serde::Serialize,
243 |         serde::Deserialize,
244 |         extism_pdk::FromBytes,
245 |         extism_pdk::ToBytes,
246 |     )]
247 |     #[encoding(Json)]
248 |     pub enum Role {
249 |         #[default]
250 |         #[serde(rename = "assistant")]
251 |         Assistant,
252 |         #[serde(rename = "user")]
253 |         User,
254 |     }
255 | 
256 |     #[derive(
257 |         Default,
258 |         Debug,
259 |         Clone,
260 |         serde::Serialize,
261 |         serde::Deserialize,
262 |         extism_pdk::FromBytes,
263 |         extism_pdk::ToBytes,
264 |     )]
265 |     #[encoding(Json)]
266 |     pub struct TextAnnotation {
267 |         /// Describes who the intended customer of this object or data is.
268 |         ///
269 |         /// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
270 |         #[serde(rename = "audience")]
271 |         pub audience: Vec<types::Role>,
272 | 
273 |         /// Describes how important this data is for operating the server.
274 |         ///
275 |         /// A value of 1 means "most important," and indicates that the data is
276 |         /// effectively required, while 0 means "least important," and indicates that
277 |         /// the data is entirely optional.
278 |         #[serde(rename = "priority")]
279 |         pub priority: f32,
280 |     }
281 | 
282 |     #[derive(
283 |         Default,
284 |         Debug,
285 |         Clone,
286 |         serde::Serialize,
287 |         serde::Deserialize,
288 |         extism_pdk::FromBytes,
289 |         extism_pdk::ToBytes,
290 |     )]
291 |     #[encoding(Json)]
292 |     pub struct TextResourceContents {
293 |         /// The MIME type of this resource, if known.
294 |         #[serde(rename = "mimeType")]
295 |         #[serde(skip_serializing_if = "Option::is_none")]
296 |         #[serde(default)]
297 |         pub mime_type: Option<String>,
298 | 
299 |         /// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
300 |         #[serde(rename = "text")]
301 |         pub text: String,
302 | 
303 |         /// The URI of this resource.
304 |         #[serde(rename = "uri")]
305 |         pub uri: String,
306 |     }
307 | 
308 |     #[derive(
309 |         Default,
310 |         Debug,
311 |         Clone,
312 |         serde::Serialize,
313 |         serde::Deserialize,
314 |         extism_pdk::FromBytes,
315 |         extism_pdk::ToBytes,
316 |     )]
317 |     #[encoding(Json)]
318 |     pub struct ToolDescription {
319 |         /// A description of the tool
320 |         #[serde(rename = "description")]
321 |         pub description: String,
322 | 
323 |         /// The JSON schema describing the argument input
324 |         #[serde(rename = "inputSchema")]
325 |         pub input_schema: serde_json::Map<String, serde_json::Value>,
326 | 
327 |         /// The name of the tool. It should match the plugin / binding name.
328 |         #[serde(rename = "name")]
329 |         pub name: String,
330 |     }
331 | }
332 | 
333 | mod raw_imports {
334 |     use super::*;
335 |     #[host_fn]
336 |     extern "ExtismHost" {}
337 | }
338 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/qr-code/src/pdk.rs:
--------------------------------------------------------------------------------

```rust
  1 | // THIS FILE WAS GENERATED BY `xtp-rust-bindgen`. DO NOT EDIT.
  2 | 
  3 | #![allow(non_snake_case)]
  4 | #![allow(unused_macros)]
  5 | use extism_pdk::*;
  6 | 
  7 | #[allow(unused)]
  8 | fn panic_if_key_missing() -> ! {
  9 |     panic!("missing key");
 10 | }
 11 | 
 12 | pub(crate) mod internal {
 13 |     pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
 14 |         let err = format!("{:?}", e);
 15 |         let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
 16 |         unsafe {
 17 |             extism_pdk::extism::error_set(mem.offset());
 18 |         }
 19 |         -1
 20 |     }
 21 | }
 22 | 
 23 | #[allow(unused)]
 24 | macro_rules! try_input {
 25 |     () => {{
 26 |         let x = extism_pdk::input();
 27 |         match x {
 28 |             Ok(x) => x,
 29 |             Err(e) => return internal::return_error(e),
 30 |         }
 31 |     }};
 32 | }
 33 | 
 34 | #[allow(unused)]
 35 | macro_rules! try_input_json {
 36 |     () => {{
 37 |         let x = extism_pdk::input();
 38 |         match x {
 39 |             Ok(extism_pdk::Json(x)) => x,
 40 |             Err(e) => return internal::return_error(e),
 41 |         }
 42 |     }};
 43 | }
 44 | 
 45 | use base64_serde::base64_serde_type;
 46 | 
 47 | base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
 48 | 
 49 | mod exports {
 50 |     use super::*;
 51 | 
 52 |     #[unsafe(no_mangle)]
 53 |     pub extern "C" fn call() -> i32 {
 54 |         let ret =
 55 |             crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 56 | 
 57 |         match ret {
 58 |             Ok(()) => 0,
 59 |             Err(e) => internal::return_error(e),
 60 |         }
 61 |     }
 62 | 
 63 |     #[unsafe(no_mangle)]
 64 |     pub extern "C" fn describe() -> i32 {
 65 |         let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 66 | 
 67 |         match ret {
 68 |             Ok(()) => 0,
 69 |             Err(e) => internal::return_error(e),
 70 |         }
 71 |     }
 72 | }
 73 | 
 74 | pub mod types {
 75 |     use super::*;
 76 | 
 77 |     #[derive(
 78 |         Default,
 79 |         Debug,
 80 |         Clone,
 81 |         serde::Serialize,
 82 |         serde::Deserialize,
 83 |         extism_pdk::FromBytes,
 84 |         extism_pdk::ToBytes,
 85 |     )]
 86 |     #[encoding(Json)]
 87 |     pub struct BlobResourceContents {
 88 |         /// A base64-encoded string representing the binary data of the item.
 89 |         #[serde(rename = "blob")]
 90 |         pub blob: String,
 91 | 
 92 |         /// The MIME type of this resource, if known.
 93 |         #[serde(rename = "mimeType")]
 94 |         #[serde(skip_serializing_if = "Option::is_none")]
 95 |         #[serde(default)]
 96 |         pub mime_type: Option<String>,
 97 | 
 98 |         /// The URI of this resource.
 99 |         #[serde(rename = "uri")]
100 |         pub uri: String,
101 |     }
102 | 
103 |     #[derive(
104 |         Default,
105 |         Debug,
106 |         Clone,
107 |         serde::Serialize,
108 |         serde::Deserialize,
109 |         extism_pdk::FromBytes,
110 |         extism_pdk::ToBytes,
111 |     )]
112 |     #[encoding(Json)]
113 |     pub struct CallToolRequest {
114 |         #[serde(rename = "method")]
115 |         #[serde(skip_serializing_if = "Option::is_none")]
116 |         #[serde(default)]
117 |         pub method: Option<String>,
118 | 
119 |         #[serde(rename = "params")]
120 |         pub params: types::Params,
121 |     }
122 | 
123 |     #[derive(
124 |         Default,
125 |         Debug,
126 |         Clone,
127 |         serde::Serialize,
128 |         serde::Deserialize,
129 |         extism_pdk::FromBytes,
130 |         extism_pdk::ToBytes,
131 |     )]
132 |     #[encoding(Json)]
133 |     pub struct CallToolResult {
134 |         #[serde(rename = "content")]
135 |         pub content: Vec<types::Content>,
136 | 
137 |         /// Whether the tool call ended in an error.
138 |         ///
139 |         /// If not set, this is assumed to be false (the call was successful).
140 |         #[serde(rename = "isError")]
141 |         #[serde(skip_serializing_if = "Option::is_none")]
142 |         #[serde(default)]
143 |         pub is_error: Option<bool>,
144 |     }
145 | 
146 |     #[derive(
147 |         Default,
148 |         Debug,
149 |         Clone,
150 |         serde::Serialize,
151 |         serde::Deserialize,
152 |         extism_pdk::FromBytes,
153 |         extism_pdk::ToBytes,
154 |     )]
155 |     #[encoding(Json)]
156 |     pub struct Content {
157 |         #[serde(rename = "annotations")]
158 |         #[serde(skip_serializing_if = "Option::is_none")]
159 |         #[serde(default)]
160 |         pub annotations: Option<types::TextAnnotation>,
161 | 
162 |         /// The base64-encoded image data.
163 |         #[serde(rename = "data")]
164 |         #[serde(skip_serializing_if = "Option::is_none")]
165 |         #[serde(default)]
166 |         pub data: Option<String>,
167 | 
168 |         /// The MIME type of the image. Different providers may support different image types.
169 |         #[serde(rename = "mimeType")]
170 |         #[serde(skip_serializing_if = "Option::is_none")]
171 |         #[serde(default)]
172 |         pub mime_type: Option<String>,
173 | 
174 |         /// The text content of the message.
175 |         #[serde(rename = "text")]
176 |         #[serde(skip_serializing_if = "Option::is_none")]
177 |         #[serde(default)]
178 |         pub text: Option<String>,
179 | 
180 |         #[serde(rename = "type")]
181 |         pub r#type: types::ContentType,
182 |     }
183 | 
184 |     #[derive(
185 |         Default,
186 |         Debug,
187 |         Clone,
188 |         serde::Serialize,
189 |         serde::Deserialize,
190 |         extism_pdk::FromBytes,
191 |         extism_pdk::ToBytes,
192 |     )]
193 |     #[encoding(Json)]
194 |     pub enum ContentType {
195 |         #[default]
196 |         #[serde(rename = "text")]
197 |         Text,
198 |         #[serde(rename = "image")]
199 |         Image,
200 |         #[serde(rename = "resource")]
201 |         Resource,
202 |     }
203 | 
204 |     #[derive(
205 |         Default,
206 |         Debug,
207 |         Clone,
208 |         serde::Serialize,
209 |         serde::Deserialize,
210 |         extism_pdk::FromBytes,
211 |         extism_pdk::ToBytes,
212 |     )]
213 |     #[encoding(Json)]
214 |     pub struct ListToolsResult {
215 |         /// The list of ToolDescription objects provided by this servlet.
216 |         #[serde(rename = "tools")]
217 |         pub tools: Vec<types::ToolDescription>,
218 |     }
219 | 
220 |     #[derive(
221 |         Default,
222 |         Debug,
223 |         Clone,
224 |         serde::Serialize,
225 |         serde::Deserialize,
226 |         extism_pdk::FromBytes,
227 |         extism_pdk::ToBytes,
228 |     )]
229 |     #[encoding(Json)]
230 |     pub struct Params {
231 |         #[serde(rename = "arguments")]
232 |         #[serde(skip_serializing_if = "Option::is_none")]
233 |         #[serde(default)]
234 |         pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
235 | 
236 |         #[serde(rename = "name")]
237 |         pub name: String,
238 |     }
239 | 
240 |     #[derive(
241 |         Default,
242 |         Debug,
243 |         Clone,
244 |         serde::Serialize,
245 |         serde::Deserialize,
246 |         extism_pdk::FromBytes,
247 |         extism_pdk::ToBytes,
248 |     )]
249 |     #[encoding(Json)]
250 |     pub enum Role {
251 |         #[default]
252 |         #[serde(rename = "assistant")]
253 |         Assistant,
254 |         #[serde(rename = "user")]
255 |         User,
256 |     }
257 | 
258 |     #[derive(
259 |         Default,
260 |         Debug,
261 |         Clone,
262 |         serde::Serialize,
263 |         serde::Deserialize,
264 |         extism_pdk::FromBytes,
265 |         extism_pdk::ToBytes,
266 |     )]
267 |     #[encoding(Json)]
268 |     pub struct TextAnnotation {
269 |         /// Describes who the intended customer of this object or data is.
270 |         ///
271 |         /// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
272 |         #[serde(rename = "audience")]
273 |         pub audience: Vec<types::Role>,
274 | 
275 |         /// Describes how important this data is for operating the server.
276 |         ///
277 |         /// A value of 1 means "most important," and indicates that the data is
278 |         /// effectively required, while 0 means "least important," and indicates that
279 |         /// the data is entirely optional.
280 |         #[serde(rename = "priority")]
281 |         pub priority: f32,
282 |     }
283 | 
284 |     #[derive(
285 |         Default,
286 |         Debug,
287 |         Clone,
288 |         serde::Serialize,
289 |         serde::Deserialize,
290 |         extism_pdk::FromBytes,
291 |         extism_pdk::ToBytes,
292 |     )]
293 |     #[encoding(Json)]
294 |     pub struct TextResourceContents {
295 |         /// The MIME type of this resource, if known.
296 |         #[serde(rename = "mimeType")]
297 |         #[serde(skip_serializing_if = "Option::is_none")]
298 |         #[serde(default)]
299 |         pub mime_type: Option<String>,
300 | 
301 |         /// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
302 |         #[serde(rename = "text")]
303 |         pub text: String,
304 | 
305 |         /// The URI of this resource.
306 |         #[serde(rename = "uri")]
307 |         pub uri: String,
308 |     }
309 | 
310 |     #[derive(
311 |         Default,
312 |         Debug,
313 |         Clone,
314 |         serde::Serialize,
315 |         serde::Deserialize,
316 |         extism_pdk::FromBytes,
317 |         extism_pdk::ToBytes,
318 |     )]
319 |     #[encoding(Json)]
320 |     pub struct ToolDescription {
321 |         /// A description of the tool
322 |         #[serde(rename = "description")]
323 |         pub description: String,
324 | 
325 |         /// The JSON schema describing the argument input
326 |         #[serde(rename = "inputSchema")]
327 |         pub input_schema: serde_json::Map<String, serde_json::Value>,
328 | 
329 |         /// The name of the tool. It should match the plugin / binding name.
330 |         #[serde(rename = "name")]
331 |         pub name: String,
332 |     }
333 | }
334 | 
335 | mod raw_imports {
336 |     use super::*;
337 |     #[host_fn]
338 |     extern "ExtismHost" {}
339 | }
340 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/time/src/pdk.rs:
--------------------------------------------------------------------------------

```rust
  1 | // THIS FILE WAS GENERATED BY `xtp-rust-bindgen`. DO NOT EDIT.
  2 | 
  3 | #![allow(non_snake_case)]
  4 | #![allow(unused_macros)]
  5 | use extism_pdk::*;
  6 | 
  7 | #[allow(unused)]
  8 | fn panic_if_key_missing() -> ! {
  9 |     panic!("missing key");
 10 | }
 11 | 
 12 | pub(crate) mod internal {
 13 |     pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
 14 |         let err = format!("{:?}", e);
 15 |         let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
 16 |         unsafe {
 17 |             extism_pdk::extism::error_set(mem.offset());
 18 |         }
 19 |         -1
 20 |     }
 21 | }
 22 | 
 23 | #[allow(unused)]
 24 | macro_rules! try_input {
 25 |     () => {{
 26 |         let x = extism_pdk::input();
 27 |         match x {
 28 |             Ok(x) => x,
 29 |             Err(e) => return internal::return_error(e),
 30 |         }
 31 |     }};
 32 | }
 33 | 
 34 | #[allow(unused)]
 35 | macro_rules! try_input_json {
 36 |     () => {{
 37 |         let x = extism_pdk::input();
 38 |         match x {
 39 |             Ok(extism_pdk::Json(x)) => x,
 40 |             Err(e) => return internal::return_error(e),
 41 |         }
 42 |     }};
 43 | }
 44 | 
 45 | use base64_serde::base64_serde_type;
 46 | 
 47 | base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
 48 | 
 49 | mod exports {
 50 |     use super::*;
 51 | 
 52 |     #[unsafe(no_mangle)]
 53 |     pub extern "C" fn call() -> i32 {
 54 |         let ret =
 55 |             crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 56 | 
 57 |         match ret {
 58 |             Ok(()) => 0,
 59 |             Err(e) => internal::return_error(e),
 60 |         }
 61 |     }
 62 | 
 63 |     #[unsafe(no_mangle)]
 64 |     pub extern "C" fn describe() -> i32 {
 65 |         let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
 66 | 
 67 |         match ret {
 68 |             Ok(()) => 0,
 69 |             Err(e) => internal::return_error(e),
 70 |         }
 71 |     }
 72 | }
 73 | 
 74 | pub mod types {
 75 |     use super::*;
 76 | 
 77 |     #[derive(
 78 |         Default,
 79 |         Debug,
 80 |         Clone,
 81 |         serde::Serialize,
 82 |         serde::Deserialize,
 83 |         extism_pdk::FromBytes,
 84 |         extism_pdk::ToBytes,
 85 |     )]
 86 |     #[encoding(Json)]
 87 |     pub struct BlobResourceContents {
 88 |         /// A base64-encoded string representing the binary data of the item.
 89 |         #[serde(rename = "blob")]
 90 |         pub blob: String,
 91 | 
 92 |         /// The MIME type of this resource, if known.
 93 |         #[serde(rename = "mimeType")]
 94 |         #[serde(skip_serializing_if = "Option::is_none")]
 95 |         #[serde(default)]
 96 |         pub mime_type: Option<String>,
 97 | 
 98 |         /// The URI of this resource.
 99 |         #[serde(rename = "uri")]
100 |         pub uri: String,
101 |     }
102 | 
103 |     #[derive(
104 |         Default,
105 |         Debug,
106 |         Clone,
107 |         serde::Serialize,
108 |         serde::Deserialize,
109 |         extism_pdk::FromBytes,
110 |         extism_pdk::ToBytes,
111 |     )]
112 |     #[encoding(Json)]
113 |     pub struct CallToolRequest {
114 |         #[serde(rename = "method")]
115 |         #[serde(skip_serializing_if = "Option::is_none")]
116 |         #[serde(default)]
117 |         pub method: Option<String>,
118 | 
119 |         #[serde(rename = "params")]
120 |         pub params: types::Params,
121 |     }
122 | 
123 |     #[derive(
124 |         Default,
125 |         Debug,
126 |         Clone,
127 |         serde::Serialize,
128 |         serde::Deserialize,
129 |         extism_pdk::FromBytes,
130 |         extism_pdk::ToBytes,
131 |     )]
132 |     #[encoding(Json)]
133 |     pub struct CallToolResult {
134 |         #[serde(rename = "content")]
135 |         pub content: Vec<types::Content>,
136 | 
137 |         /// Whether the tool call ended in an error.
138 |         ///
139 |         /// If not set, this is assumed to be false (the call was successful).
140 |         #[serde(rename = "isError")]
141 |         #[serde(skip_serializing_if = "Option::is_none")]
142 |         #[serde(default)]
143 |         pub is_error: Option<bool>,
144 |     }
145 | 
146 |     #[derive(
147 |         Default,
148 |         Debug,
149 |         Clone,
150 |         serde::Serialize,
151 |         serde::Deserialize,
152 |         extism_pdk::FromBytes,
153 |         extism_pdk::ToBytes,
154 |     )]
155 |     #[encoding(Json)]
156 |     pub struct Content {
157 |         #[serde(rename = "annotations")]
158 |         #[serde(skip_serializing_if = "Option::is_none")]
159 |         #[serde(default)]
160 |         pub annotations: Option<types::TextAnnotation>,
161 | 
162 |         /// The base64-encoded image data.
163 |         #[serde(rename = "data")]
164 |         #[serde(skip_serializing_if = "Option::is_none")]
165 |         #[serde(default)]
166 |         pub data: Option<String>,
167 | 
168 |         /// The MIME type of the image. Different providers may support different image types.
169 |         #[serde(rename = "mimeType")]
170 |         #[serde(skip_serializing_if = "Option::is_none")]
171 |         #[serde(default)]
172 |         pub mime_type: Option<String>,
173 | 
174 |         /// The text content of the message.
175 |         #[serde(rename = "text")]
176 |         #[serde(skip_serializing_if = "Option::is_none")]
177 |         #[serde(default)]
178 |         pub text: Option<String>,
179 | 
180 |         #[serde(rename = "type")]
181 |         pub r#type: types::ContentType,
182 |     }
183 | 
184 |     #[derive(
185 |         Default,
186 |         Debug,
187 |         Clone,
188 |         serde::Serialize,
189 |         serde::Deserialize,
190 |         extism_pdk::FromBytes,
191 |         extism_pdk::ToBytes,
192 |     )]
193 |     #[encoding(Json)]
194 |     pub enum ContentType {
195 |         #[default]
196 |         #[serde(rename = "text")]
197 |         Text,
198 |         #[serde(rename = "image")]
199 |         Image,
200 |         #[serde(rename = "resource")]
201 |         Resource,
202 |     }
203 | 
204 |     #[derive(
205 |         Default,
206 |         Debug,
207 |         Clone,
208 |         serde::Serialize,
209 |         serde::Deserialize,
210 |         extism_pdk::FromBytes,
211 |         extism_pdk::ToBytes,
212 |     )]
213 |     #[encoding(Json)]
214 |     pub struct ListToolsResult {
215 |         /// The list of ToolDescription objects provided by this servlet.
216 |         #[serde(rename = "tools")]
217 |         pub tools: Vec<types::ToolDescription>,
218 |     }
219 | 
220 |     #[derive(
221 |         Default,
222 |         Debug,
223 |         Clone,
224 |         serde::Serialize,
225 |         serde::Deserialize,
226 |         extism_pdk::FromBytes,
227 |         extism_pdk::ToBytes,
228 |     )]
229 |     #[encoding(Json)]
230 |     pub struct Params {
231 |         #[serde(rename = "arguments")]
232 |         #[serde(skip_serializing_if = "Option::is_none")]
233 |         #[serde(default)]
234 |         pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
235 | 
236 |         #[serde(rename = "name")]
237 |         pub name: String,
238 |     }
239 | 
240 |     #[derive(
241 |         Default,
242 |         Debug,
243 |         Clone,
244 |         serde::Serialize,
245 |         serde::Deserialize,
246 |         extism_pdk::FromBytes,
247 |         extism_pdk::ToBytes,
248 |     )]
249 |     #[encoding(Json)]
250 |     pub enum Role {
251 |         #[default]
252 |         #[serde(rename = "assistant")]
253 |         Assistant,
254 |         #[serde(rename = "user")]
255 |         User,
256 |     }
257 | 
258 |     #[derive(
259 |         Default,
260 |         Debug,
261 |         Clone,
262 |         serde::Serialize,
263 |         serde::Deserialize,
264 |         extism_pdk::FromBytes,
265 |         extism_pdk::ToBytes,
266 |     )]
267 |     #[encoding(Json)]
268 |     pub struct TextAnnotation {
269 |         /// Describes who the intended customer of this object or data is.
270 |         ///
271 |         /// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
272 |         #[serde(rename = "audience")]
273 |         pub audience: Vec<types::Role>,
274 | 
275 |         /// Describes how important this data is for operating the server.
276 |         ///
277 |         /// A value of 1 means "most important," and indicates that the data is
278 |         /// effectively required, while 0 means "least important," and indicates that
279 |         /// the data is entirely optional.
280 |         #[serde(rename = "priority")]
281 |         pub priority: f32,
282 |     }
283 | 
284 |     #[derive(
285 |         Default,
286 |         Debug,
287 |         Clone,
288 |         serde::Serialize,
289 |         serde::Deserialize,
290 |         extism_pdk::FromBytes,
291 |         extism_pdk::ToBytes,
292 |     )]
293 |     #[encoding(Json)]
294 |     pub struct TextResourceContents {
295 |         /// The MIME type of this resource, if known.
296 |         #[serde(rename = "mimeType")]
297 |         #[serde(skip_serializing_if = "Option::is_none")]
298 |         #[serde(default)]
299 |         pub mime_type: Option<String>,
300 | 
301 |         /// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
302 |         #[serde(rename = "text")]
303 |         pub text: String,
304 | 
305 |         /// The URI of this resource.
306 |         #[serde(rename = "uri")]
307 |         pub uri: String,
308 |     }
309 | 
310 |     #[derive(
311 |         Default,
312 |         Debug,
313 |         Clone,
314 |         serde::Serialize,
315 |         serde::Deserialize,
316 |         extism_pdk::FromBytes,
317 |         extism_pdk::ToBytes,
318 |     )]
319 |     #[encoding(Json)]
320 |     pub struct ToolDescription {
321 |         /// A description of the tool
322 |         #[serde(rename = "description")]
323 |         pub description: String,
324 | 
325 |         /// The JSON schema describing the argument input
326 |         #[serde(rename = "inputSchema")]
327 |         pub input_schema: serde_json::Map<String, serde_json::Value>,
328 | 
329 |         /// The name of the tool. It should match the plugin / binding name.
330 |         #[serde(rename = "name")]
331 |         pub name: String,
332 |     }
333 | }
334 | 
335 | mod raw_imports {
336 |     use super::*;
337 |     #[host_fn]
338 |     extern "ExtismHost" {}
339 | }
340 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/qdrant/src/lib.rs:
--------------------------------------------------------------------------------

```rust
  1 | mod pdk;
  2 | mod qdrant_client;
  3 | 
  4 | use extism_pdk::*;
  5 | use pdk::types::{
  6 |     CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
  7 | };
  8 | use qdrant_client::*;
  9 | use serde_json::json;
 10 | 
 11 | fn get_qdrant_client() -> Result<QdrantClient, Error> {
 12 |     let qdrant_url = config::get("QDRANT_URL")?
 13 |         .ok_or_else(|| Error::msg("QDRANT_URL configuration is required but not set"))?;
 14 | 
 15 |     let mut client = QdrantClient::new_with_url(qdrant_url);
 16 | 
 17 |     // Check if API key is set in config
 18 |     if let Ok(Some(api_key)) = config::get("QDRANT_API_KEY") {
 19 |         client.set_api_key(api_key);
 20 |     }
 21 | 
 22 |     Ok(client)
 23 | }
 24 | 
 25 | fn ensure_collection_exists(
 26 |     client: &QdrantClient,
 27 |     collection_name: &str,
 28 |     vector_size: u32,
 29 | ) -> Result<(), Error> {
 30 |     // check if the collection exists. If present, delete it.
 31 |     if let Ok(true) = client.collection_exists(collection_name) {
 32 |         println!("Collection `{}` exists", collection_name);
 33 |         match client.delete_collection(collection_name) {
 34 |             Ok(_) => println!("Collection `{}` deleted", collection_name),
 35 |             Err(e) => println!("Error deleting collection: {:?}", e),
 36 |         }
 37 |     };
 38 | 
 39 |     // Create collection
 40 |     let create_result = client.create_collection(collection_name, vector_size);
 41 |     println!("Create collection result is {:?}", create_result);
 42 | 
 43 |     Ok(())
 44 | }
 45 | 
 46 | pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
 47 |     match input.params.name.as_str() {
 48 |         "qdrant_store" => qdrant_store(input),
 49 |         "qdrant_find" => qdrant_find(input),
 50 |         "qdrant_create_collection" => qdrant_create_collection(input),
 51 |         _ => Ok(CallToolResult {
 52 |             is_error: Some(true),
 53 |             content: vec![Content {
 54 |                 annotations: None,
 55 |                 text: Some(format!("Unknown tool: {}", input.params.name)),
 56 |                 mime_type: None,
 57 |                 r#type: ContentType::Text,
 58 |                 data: None,
 59 |             }],
 60 |         }),
 61 |     }
 62 | }
 63 | 
 64 | fn qdrant_store(input: CallToolRequest) -> Result<CallToolResult, Error> {
 65 |     let args = input.params.arguments.unwrap_or_default();
 66 | 
 67 |     let collection_name = args
 68 |         .get("collection_name")
 69 |         .and_then(|v| v.as_str())
 70 |         .ok_or_else(|| Error::msg("collection_name parameter is required"))?;
 71 | 
 72 |     let vector = args
 73 |         .get("vector")
 74 |         .and_then(|v| v.as_array())
 75 |         .ok_or_else(|| Error::msg("vector parameter is required"))?
 76 |         .iter()
 77 |         .map(|v| v.as_f64().unwrap_or_default())
 78 |         .collect::<Vec<f64>>();
 79 | 
 80 |     let text = args
 81 |         .get("text")
 82 |         .and_then(|v| v.as_str())
 83 |         .ok_or_else(|| Error::msg("text parameter is required"))?;
 84 | 
 85 |     let client = get_qdrant_client()?;
 86 |     ensure_collection_exists(&client, collection_name, vector.len() as u32)?;
 87 | 
 88 |     let point_id = uuid::Uuid::new_v4().to_string();
 89 |     let vector: Vec<f32> = vector.into_iter().map(|x| x as f32).collect();
 90 | 
 91 |     let mut points = Vec::new();
 92 |     points.push(Point {
 93 |         id: PointId::Uuid(point_id.clone()),
 94 |         vector,
 95 |         payload: json!({
 96 |             "text": text,
 97 |             "metadata": {},
 98 |         })
 99 |         .as_object()
100 |         .map(|m| m.to_owned()),
101 |     });
102 | 
103 |     client.upsert_points(collection_name, points)?;
104 |     println!("Upsert points result is {:?}", ());
105 | 
106 |     Ok(CallToolResult {
107 |         is_error: None,
108 |         content: vec![Content {
109 |             annotations: None,
110 |             text: Some(format!(
111 |                 "Successfully stored document with ID: {}",
112 |                 point_id
113 |             )),
114 |             mime_type: None,
115 |             r#type: ContentType::Text,
116 |             data: None,
117 |         }],
118 |     })
119 | }
120 | 
121 | fn qdrant_find(input: CallToolRequest) -> Result<CallToolResult, Error> {
122 |     let args = input.params.arguments.unwrap_or_default();
123 | 
124 |     let collection_name = args
125 |         .get("collection_name")
126 |         .and_then(|v| v.as_str())
127 |         .ok_or_else(|| Error::msg("collection_name parameter is required"))?;
128 | 
129 |     let vector = args
130 |         .get("vector")
131 |         .and_then(|v| v.as_array())
132 |         .ok_or_else(|| Error::msg("vector parameter is required"))?
133 |         .iter()
134 |         .map(|v| v.as_f64().unwrap_or_default())
135 |         .collect::<Vec<f64>>();
136 | 
137 |     let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(5);
138 | 
139 |     let client = get_qdrant_client()?;
140 | 
141 |     let vector_f32: Vec<f32> = vector.into_iter().map(|x| x as f32).collect();
142 |     let search_result = client.search_points(collection_name, vector_f32, limit, None)?;
143 | 
144 |     let mut results = Vec::new();
145 |     for point in search_result {
146 |         if let Some(payload) = &point.payload {
147 |             if let Some(text) = payload.get("text").and_then(|v| v.as_str()) {
148 |                 results.push(format!("Score: {:.4} - {}", point.score, text));
149 |             }
150 |         }
151 |     }
152 | 
153 |     Ok(CallToolResult {
154 |         is_error: None,
155 |         content: vec![Content {
156 |             annotations: None,
157 |             text: Some(results.join("\n")),
158 |             mime_type: None,
159 |             r#type: ContentType::Text,
160 |             data: None,
161 |         }],
162 |     })
163 | }
164 | 
165 | fn qdrant_create_collection(input: CallToolRequest) -> Result<CallToolResult, Error> {
166 |     let args = input.params.arguments.unwrap_or_default();
167 | 
168 |     let collection_name = args
169 |         .get("collection_name")
170 |         .and_then(|v| v.as_str())
171 |         .ok_or_else(|| Error::msg("collection_name parameter is required"))?;
172 | 
173 |     let vector_size = args
174 |         .get("vector_size")
175 |         .and_then(|v| v.as_u64())
176 |         .unwrap_or(384) as u32;
177 | 
178 |     let client = get_qdrant_client()?;
179 |     ensure_collection_exists(&client, collection_name, vector_size)?;
180 | 
181 |     Ok(CallToolResult {
182 |         is_error: None,
183 |         content: vec![Content {
184 |             annotations: None,
185 |             text: Some(format!(
186 |                 "Successfully created collection '{}' with vector size {}",
187 |                 collection_name, vector_size
188 |             )),
189 |             mime_type: None,
190 |             r#type: ContentType::Text,
191 |             data: None,
192 |         }],
193 |     })
194 | }
195 | 
196 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
197 |     Ok(ListToolsResult {
198 |         tools: vec![
199 |             ToolDescription {
200 |                 name: "qdrant_create_collection".into(),
201 |                 description: "Creates a new collection in Qdrant with specified vector size".into(),
202 |                 input_schema: json!({
203 |                     "type": "object",
204 |                     "properties": {
205 |                         "collection_name": {
206 |                             "type": "string",
207 |                             "description": "The name of the collection to create",
208 |                         },
209 |                         "vector_size": {
210 |                             "type": "integer",
211 |                             "description": "The size of vectors to be stored in this collection",
212 |                             "default": 384
213 |                         }
214 |                     },
215 |                     "required": ["collection_name"],
216 |                 })
217 |                 .as_object()
218 |                 .unwrap()
219 |                 .clone(),
220 |             },
221 |             ToolDescription {
222 |                 name: "qdrant_store".into(),
223 |                 description: "Stores a document with its vector embedding in Qdrant.".into(),
224 |                 input_schema: json!({
225 |                     "type": "object",
226 |                     "properties": {
227 |                         "collection_name": {
228 |                             "type": "string",
229 |                             "description": "The name of the collection to store the document in",
230 |                         },
231 |                         "text": {
232 |                             "type": "string",
233 |                             "description": "The text content to store",
234 |                         },
235 |                         "vector": {
236 |                             "type": "array",
237 |                             "items": {"type": "number"},
238 |                             "description": "The vector embedding of the text.",
239 |                         }
240 |                     },
241 |                     "required": ["collection_name", "text", "vector"],
242 |                 })
243 |                 .as_object()
244 |                 .unwrap()
245 |                 .clone(),
246 |             },
247 |             ToolDescription {
248 |                 name: "qdrant_find".into(),
249 |                 description: "Finds similar documents in Qdrant using vector similarity search"
250 |                     .into(),
251 |                 input_schema: json!({
252 |                     "type": "object",
253 |                     "properties": {
254 |                         "collection_name": {
255 |                             "type": "string",
256 |                             "description": "The name of the collection to search in",
257 |                         },
258 |                         "vector": {
259 |                             "type": "array",
260 |                             "items": {"type": "number"},
261 |                             "description": "The query vector to search with.",
262 |                         },
263 |                         "limit": {
264 |                             "type": "integer",
265 |                             "description": "Maximum number of results to return",
266 |                             "default": 5
267 |                         }
268 |                     },
269 |                     "required": ["collection_name", "vector"],
270 |                 })
271 |                 .as_object()
272 |                 .unwrap()
273 |                 .clone(),
274 |             },
275 |         ],
276 |     })
277 | }
278 | 
```

--------------------------------------------------------------------------------
/src/https_auth.rs:
--------------------------------------------------------------------------------

```rust
  1 | use crate::config::AuthConfig;
  2 | use reqwest::RequestBuilder;
  3 | use std::{cmp::Reverse, collections::HashMap};
  4 | use url::Url;
  5 | 
  6 | pub trait Authenticator {
  7 |     /// Adds authentication headers to the request if present in auths.
  8 |     fn add_auth(self, auths: &Option<HashMap<Url, AuthConfig>>, url: &Url) -> RequestBuilder;
  9 | }
 10 | 
 11 | impl Authenticator for RequestBuilder {
 12 |     fn add_auth(self, auths: &Option<HashMap<Url, AuthConfig>>, url: &Url) -> RequestBuilder {
 13 |         if let Some(auths) = auths {
 14 |             let mut auths: Vec<(&str, &AuthConfig)> =
 15 |                 auths.iter().map(|(k, v)| (k.as_str(), v)).collect();
 16 |             auths.sort_by_key(|c| Reverse(c.0.len()));
 17 |             let url = url.to_string();
 18 |             for (k, v) in auths {
 19 |                 if url.starts_with(k) {
 20 |                     return match v {
 21 |                         AuthConfig::Basic { username, password } => {
 22 |                             self.basic_auth(username, Some(password))
 23 |                         }
 24 |                         AuthConfig::Token { token } => self.bearer_auth(token),
 25 |                     };
 26 |                 }
 27 |             }
 28 |         }
 29 | 
 30 |         self
 31 |     }
 32 | }
 33 | 
 34 | #[cfg(test)]
 35 | mod tests {
 36 |     use super::*;
 37 |     use reqwest::Client;
 38 |     use std::collections::HashMap;
 39 |     use url::Url;
 40 | 
 41 |     #[test]
 42 |     fn test_add_auth_basic_authentication() {
 43 |         let client = Client::new();
 44 |         let mut auths = HashMap::new();
 45 | 
 46 |         let url = Url::parse("https://api.example.com").unwrap();
 47 |         auths.insert(
 48 |             url.clone(),
 49 |             AuthConfig::Basic {
 50 |                 username: "testuser".to_string(),
 51 |                 password: "testpass".to_string(),
 52 |             },
 53 |         );
 54 | 
 55 |         let request = client.get("https://api.example.com/endpoint");
 56 |         let authenticated_request = request.add_auth(&Some(auths), &url);
 57 | 
 58 |         // We can't easily test the actual header since reqwest doesn't expose it,
 59 |         // but we can verify the method doesn't panic and returns a RequestBuilder
 60 |         // The fact that we got here without panicking means the method worked
 61 |         drop(authenticated_request);
 62 |     }
 63 | 
 64 |     #[test]
 65 |     fn test_add_auth_token_authentication() {
 66 |         let client = Client::new();
 67 |         let mut auths = HashMap::new();
 68 | 
 69 |         let url = Url::parse("https://api.example.com").unwrap();
 70 |         auths.insert(
 71 |             url.clone(),
 72 |             AuthConfig::Token {
 73 |                 token: "bearer-token-123".to_string(),
 74 |             },
 75 |         );
 76 | 
 77 |         let request = client.get("https://api.example.com/endpoint");
 78 |         let authenticated_request = request.add_auth(&Some(auths), &url);
 79 | 
 80 |         // Verify the method completes without error
 81 |         // The fact that we got here without panicking means the method worked
 82 |         drop(authenticated_request);
 83 |     }
 84 | 
 85 |     #[test]
 86 |     fn test_add_auth_no_auths_provided() {
 87 |         let client = Client::new();
 88 |         let url = Url::parse("https://api.example.com").unwrap();
 89 | 
 90 |         let request = client.get("https://api.example.com/endpoint");
 91 |         let result_request = request.add_auth(&None, &url);
 92 | 
 93 |         // Should return the original request unchanged
 94 |         // The fact that we got here without panicking means the method worked
 95 |         drop(result_request);
 96 |     }
 97 | 
 98 |     #[test]
 99 |     fn test_add_auth_empty_auths_map() {
100 |         let client = Client::new();
101 |         let auths = HashMap::new();
102 |         let url = Url::parse("https://api.example.com").unwrap();
103 | 
104 |         let request = client.get("https://api.example.com/endpoint");
105 |         let result_request = request.add_auth(&Some(auths), &url);
106 | 
107 |         // Should return the original request unchanged when no matching auth
108 |         // The fact that we got here without panicking means the method worked
109 |         drop(result_request);
110 |     }
111 | 
112 |     #[test]
113 |     fn test_add_auth_url_prefix_matching() {
114 |         let client = Client::new();
115 |         let mut auths = HashMap::new();
116 | 
117 |         // Add auth for broader domain
118 |         let domain_url = Url::parse("https://example.com").unwrap();
119 |         auths.insert(
120 |             domain_url,
121 |             AuthConfig::Basic {
122 |                 username: "domain_user".to_string(),
123 |                 password: "domain_pass".to_string(),
124 |             },
125 |         );
126 | 
127 |         // Add auth for specific API endpoint (longer prefix)
128 |         let api_url = Url::parse("https://example.com/api").unwrap();
129 |         auths.insert(
130 |             api_url,
131 |             AuthConfig::Token {
132 |                 token: "api-token".to_string(),
133 |             },
134 |         );
135 | 
136 |         // Test that longer prefix wins
137 |         let target_url = Url::parse("https://example.com/api/v1/data").unwrap();
138 |         let request = client.get(target_url.as_str());
139 |         let authenticated_request = request.add_auth(&Some(auths), &target_url);
140 | 
141 |         // The API token should be used (longest prefix)
142 |         // The fact that we got here without panicking means the method worked
143 |         drop(authenticated_request);
144 |     }
145 | 
146 |     #[test]
147 |     fn test_add_auth_url_no_match() {
148 |         let client = Client::new();
149 |         let mut auths = HashMap::new();
150 | 
151 |         let auth_url = Url::parse("https://api.example.com").unwrap();
152 |         auths.insert(
153 |             auth_url,
154 |             AuthConfig::Basic {
155 |                 username: "testuser".to_string(),
156 |                 password: "testpass".to_string(),
157 |             },
158 |         );
159 | 
160 |         // Request to different domain
161 |         let target_url = Url::parse("https://different.com/endpoint").unwrap();
162 |         let request = client.get(target_url.as_str());
163 |         let result_request = request.add_auth(&Some(auths), &target_url);
164 | 
165 |         // Should return the original request unchanged when no URL match
166 |         // The fact that we got here without panicking means the method worked
167 |         drop(result_request);
168 |     }
169 | 
170 |     #[test]
171 |     fn test_add_auth_multiple_auths_longest_prefix_wins() {
172 |         let client = Client::new();
173 |         let mut auths = HashMap::new();
174 | 
175 |         // Add multiple auths with different prefix lengths
176 |         auths.insert(
177 |             Url::parse("https://example.com").unwrap(),
178 |             AuthConfig::Basic {
179 |                 username: "broad_user".to_string(),
180 |                 password: "broad_pass".to_string(),
181 |             },
182 |         );
183 | 
184 |         auths.insert(
185 |             Url::parse("https://example.com/api").unwrap(),
186 |             AuthConfig::Token {
187 |                 token: "api_token".to_string(),
188 |             },
189 |         );
190 | 
191 |         auths.insert(
192 |             Url::parse("https://example.com/api/v1").unwrap(),
193 |             AuthConfig::Basic {
194 |                 username: "v1_user".to_string(),
195 |                 password: "v1_pass".to_string(),
196 |             },
197 |         );
198 | 
199 |         // Test with URL that matches all three (longest should win)
200 |         let target_url = Url::parse("https://example.com/api/v1/endpoint").unwrap();
201 |         let request = client.get(target_url.as_str());
202 |         let authenticated_request = request.add_auth(&Some(auths), &target_url);
203 | 
204 |         // Should use the v1 auth (longest prefix)
205 |         // The fact that we got here without panicking means the method worked
206 |         drop(authenticated_request);
207 |     }
208 | 
209 |     #[test]
210 |     fn test_add_auth_exact_url_match() {
211 |         let client = Client::new();
212 |         let mut auths = HashMap::new();
213 | 
214 |         let exact_url = Url::parse("https://api.example.com/v1/data").unwrap();
215 |         auths.insert(
216 |             exact_url.clone(),
217 |             AuthConfig::Token {
218 |                 token: "exact-match-token".to_string(),
219 |             },
220 |         );
221 | 
222 |         let request = client.get(exact_url.as_str());
223 |         let authenticated_request = request.add_auth(&Some(auths), &exact_url);
224 | 
225 |         // The fact that we got here without panicking means the method worked
226 |         drop(authenticated_request);
227 |     }
228 | 
229 |     #[test]
230 |     fn test_add_auth_case_sensitive_urls() {
231 |         let client = Client::new();
232 |         let mut auths = HashMap::new();
233 | 
234 |         let auth_url = Url::parse("https://API.EXAMPLE.COM").unwrap();
235 |         auths.insert(
236 |             auth_url,
237 |             AuthConfig::Basic {
238 |                 username: "testuser".to_string(),
239 |                 password: "testpass".to_string(),
240 |             },
241 |         );
242 | 
243 |         // Test with lowercase URL
244 |         let target_url = Url::parse("https://api.example.com/endpoint").unwrap();
245 |         let request = client.get(target_url.as_str());
246 |         let result_request = request.add_auth(&Some(auths), &target_url);
247 | 
248 |         // Should not match due to case sensitivity
249 |         // The fact that we got here without panicking means the method worked
250 |         drop(result_request);
251 |     }
252 | 
253 |     #[test]
254 |     fn test_auth_config_types_comprehensive() {
255 |         // Test all AuthConfig variants can be created and used
256 |         let basic_auth = AuthConfig::Basic {
257 |             username: "basic_user".to_string(),
258 |             password: "basic_pass".to_string(),
259 |         };
260 | 
261 |         let token_auth = AuthConfig::Token {
262 |             token: "token_value".to_string(),
263 |         };
264 | 
265 |         let client = Client::new();
266 |         let url = Url::parse("https://test.com").unwrap();
267 | 
268 |         // Test both types can be used with add_auth
269 |         let mut auths = HashMap::new();
270 |         auths.insert(url.clone(), basic_auth);
271 | 
272 |         let request1 = client.get(url.as_str());
273 |         let result1 = request1.add_auth(&Some(auths), &url);
274 |         // The fact that we got here without panicking means the method worked
275 |         drop(result1);
276 | 
277 |         let mut auths = HashMap::new();
278 |         auths.insert(url.clone(), token_auth);
279 | 
280 |         let request2 = client.get(url.as_str());
281 |         let result2 = request2.add_auth(&Some(auths), &url);
282 |         // The fact that we got here without panicking means the method worked
283 |         drop(result2);
284 |     }
285 | }
286 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/sqlite/src/lib.rs:
--------------------------------------------------------------------------------

```rust
  1 | mod pdk;
  2 | 
  3 | use extism_pdk::*;
  4 | use pdk::types::{
  5 |     CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
  6 | };
  7 | use rusqlite::Connection;
  8 | use serde_json::json;
  9 | use std::sync::Once;
 10 | 
 11 | static DB_INIT: Once = Once::new();
 12 | 
 13 | fn init_db(db_path: &str) -> Result<(), Error> {
 14 |     let _conn = Connection::open_with_flags(
 15 |         db_path,
 16 |         rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
 17 |     )?;
 18 | 
 19 |     Ok(())
 20 | }
 21 | 
 22 | fn get_db_path() -> Result<String, Error> {
 23 |     config::get("db_path")?
 24 |         .ok_or_else(|| Error::msg("db_path configuration is required but not set"))
 25 | }
 26 | 
 27 | fn execute_read_query(query: &str, db_path: &str) -> Result<String, Error> {
 28 |     let conn = Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE)?;
 29 |     let mut stmt = conn.prepare(query)?;
 30 |     let column_names: Vec<String> = stmt.column_names().into_iter().map(String::from).collect();
 31 | 
 32 |     let rows = stmt.query_map([], |row| {
 33 |         let mut map = serde_json::Map::new();
 34 |         for (i, col_name) in column_names.iter().enumerate() {
 35 |             let value = match row.get_ref(i)? {
 36 |                 rusqlite::types::ValueRef::Null => serde_json::Value::Null,
 37 |                 rusqlite::types::ValueRef::Integer(i) => json!(i),
 38 |                 rusqlite::types::ValueRef::Real(f) => json!(f),
 39 |                 rusqlite::types::ValueRef::Text(s) => json!(s),
 40 |                 rusqlite::types::ValueRef::Blob(b) => json!(b),
 41 |             };
 42 |             map.insert(col_name.clone(), value);
 43 |         }
 44 |         Ok(map)
 45 |     })?;
 46 | 
 47 |     let results: Vec<serde_json::Map<String, serde_json::Value>> =
 48 |         rows.filter_map(Result::ok).collect();
 49 |     Ok(serde_json::to_string(&results)?)
 50 | }
 51 | 
 52 | fn execute_write_query(query: &str, db_path: &str) -> Result<String, Error> {
 53 |     let conn = Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE)?;
 54 |     let affected = conn.execute(query, [])?;
 55 |     Ok(json!({ "rows_affected": affected }).to_string())
 56 | }
 57 | 
 58 | fn create_table(query: &str, db_path: &str) -> Result<String, Error> {
 59 |     let conn = Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE)?;
 60 |     conn.execute(query, [])?;
 61 |     Ok(json!({ "status": "success" }).to_string())
 62 | }
 63 | 
 64 | fn list_tables(db_path: &str) -> Result<String, Error> {
 65 |     let conn = Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE)?;
 66 |     let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table'")?;
 67 |     let tables: Result<Vec<String>, _> = stmt.query_map([], |row| row.get(0))?.collect();
 68 |     Ok(json!({ "tables": tables? }).to_string())
 69 | }
 70 | 
 71 | fn describe_table(table_name: &str, db_path: &str) -> Result<String, Error> {
 72 |     let conn = Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE)?;
 73 |     let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", table_name))?;
 74 | 
 75 |     let columns = stmt.query_map([], |row| {
 76 |         Ok(json!({
 77 |             "cid": row.get::<_, i64>(0)?,
 78 |             "name": row.get::<_, String>(1)?,
 79 |             "type": row.get::<_, String>(2)?,
 80 |             "notnull": row.get::<_, bool>(3)?,
 81 |             "dflt_value": row.get::<_, Option<String>>(4)?,
 82 |             "pk": row.get::<_, bool>(5)?
 83 |         }))
 84 |     })?;
 85 | 
 86 |     let schema: Vec<serde_json::Value> = columns.filter_map(Result::ok).collect();
 87 |     Ok(json!({ "schema": schema }).to_string())
 88 | }
 89 | 
 90 | pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
 91 |     let db_path = get_db_path()?;
 92 |     DB_INIT.call_once(|| {
 93 |         init_db(&db_path).expect("Failed to initialize database");
 94 |     });
 95 | 
 96 |     match input.params.name.as_str() {
 97 |         "sqlite_read_query" => {
 98 |             let args = input.params.arguments.unwrap_or_default();
 99 |             let query = match args.get("query") {
100 |                 Some(v) if v.is_string() => v.as_str().unwrap(),
101 |                 _ => return Err(Error::msg("query parameter is required")),
102 |             };
103 | 
104 |             let result = execute_read_query(query, &db_path)?;
105 |             Ok(CallToolResult {
106 |                 is_error: None,
107 |                 content: vec![Content {
108 |                     annotations: None,
109 |                     text: Some(result),
110 |                     mime_type: Some("application/json".to_string()),
111 |                     r#type: ContentType::Text,
112 |                     data: None,
113 |                 }],
114 |             })
115 |         }
116 |         "sqlite_write_query" => {
117 |             let args = input.params.arguments.unwrap_or_default();
118 |             let query = match args.get("query") {
119 |                 Some(v) if v.is_string() => v.as_str().unwrap(),
120 |                 _ => return Err(Error::msg("query parameter is required")),
121 |             };
122 | 
123 |             let result = execute_write_query(query, &db_path)?;
124 |             Ok(CallToolResult {
125 |                 is_error: None,
126 |                 content: vec![Content {
127 |                     annotations: None,
128 |                     text: Some(result),
129 |                     mime_type: Some("application/json".to_string()),
130 |                     r#type: ContentType::Text,
131 |                     data: None,
132 |                 }],
133 |             })
134 |         }
135 |         "sqlite_create_table" => {
136 |             let args = input.params.arguments.unwrap_or_default();
137 |             let query = match args.get("query") {
138 |                 Some(v) if v.is_string() => v.as_str().unwrap(),
139 |                 _ => return Err(Error::msg("query parameter is required")),
140 |             };
141 | 
142 |             let result = create_table(query, &db_path)?;
143 |             Ok(CallToolResult {
144 |                 is_error: None,
145 |                 content: vec![Content {
146 |                     annotations: None,
147 |                     text: Some(result),
148 |                     mime_type: Some("application/json".to_string()),
149 |                     r#type: ContentType::Text,
150 |                     data: None,
151 |                 }],
152 |             })
153 |         }
154 |         "sqlite_list_tables" => {
155 |             let result = list_tables(&db_path)?;
156 |             Ok(CallToolResult {
157 |                 is_error: None,
158 |                 content: vec![Content {
159 |                     annotations: None,
160 |                     text: Some(result),
161 |                     mime_type: Some("application/json".to_string()),
162 |                     r#type: ContentType::Text,
163 |                     data: None,
164 |                 }],
165 |             })
166 |         }
167 |         "sqlite_describe_table" => {
168 |             let args = input.params.arguments.unwrap_or_default();
169 |             let table_name = match args.get("table_name") {
170 |                 Some(v) if v.is_string() => v.as_str().unwrap(),
171 |                 _ => return Err(Error::msg("table_name parameter is required")),
172 |             };
173 | 
174 |             let result = describe_table(table_name, &db_path)?;
175 |             Ok(CallToolResult {
176 |                 is_error: None,
177 |                 content: vec![Content {
178 |                     annotations: None,
179 |                     text: Some(result),
180 |                     mime_type: Some("application/json".to_string()),
181 |                     r#type: ContentType::Text,
182 |                     data: None,
183 |                 }],
184 |             })
185 |         }
186 |         _ => Ok(CallToolResult {
187 |             is_error: Some(true),
188 |             content: vec![Content {
189 |                 annotations: None,
190 |                 text: Some(format!("Unknown tool: {}", input.params.name)),
191 |                 mime_type: None,
192 |                 r#type: ContentType::Text,
193 |                 data: None,
194 |             }],
195 |         }),
196 |     }
197 | }
198 | 
199 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
200 |     Ok(ListToolsResult {
201 |         tools: vec![
202 |             ToolDescription {
203 |                 name: "sqlite_read_query".into(),
204 |                 description: "Execute a SELECT query on the SQLite database".into(),
205 |                 input_schema: json!({
206 |                     "type": "object",
207 |                     "properties": {
208 |                         "query": {
209 |                             "type": "string",
210 |                             "description": "SELECT SQL query to execute",
211 |                         }
212 |                     },
213 |                     "required": ["query"],
214 |                 })
215 |                 .as_object()
216 |                 .unwrap()
217 |                 .clone(),
218 |             },
219 |             ToolDescription {
220 |                 name: "sqlite_write_query".into(),
221 |                 description: "Execute an INSERT, UPDATE, or DELETE query on the SQLite database"
222 |                     .into(),
223 |                 input_schema: json!({
224 |                     "type": "object",
225 |                     "properties": {
226 |                         "query": {
227 |                             "type": "string",
228 |                             "description": "SQL query to execute",
229 |                         }
230 |                     },
231 |                     "required": ["query"],
232 |                 })
233 |                 .as_object()
234 |                 .unwrap()
235 |                 .clone(),
236 |             },
237 |             ToolDescription {
238 |                 name: "sqlite_create_table".into(),
239 |                 description: "Create a new table in the SQLite database".into(),
240 |                 input_schema: json!({
241 |                     "type": "object",
242 |                     "properties": {
243 |                         "query": {
244 |                             "type": "string",
245 |                             "description": "CREATE TABLE SQL statement",
246 |                         }
247 |                     },
248 |                     "required": ["query"],
249 |                 })
250 |                 .as_object()
251 |                 .unwrap()
252 |                 .clone(),
253 |             },
254 |             ToolDescription {
255 |                 name: "sqlite_list_tables".into(),
256 |                 description: "List all tables in the SQLite database".into(),
257 |                 input_schema: json!({
258 |                     "type": "object",
259 |                     "properties": {},
260 |                     "required": [],
261 |                 })
262 |                 .as_object()
263 |                 .unwrap()
264 |                 .clone(),
265 |             },
266 |             ToolDescription {
267 |                 name: "sqlite_describe_table".into(),
268 |                 description: "Get the schema information for a specific table".into(),
269 |                 input_schema: json!({
270 |                     "type": "object",
271 |                     "properties": {
272 |                         "table_name": {
273 |                             "type": "string",
274 |                             "description": "Name of the table to describe",
275 |                         }
276 |                     },
277 |                     "required": ["table_name"],
278 |                 })
279 |                 .as_object()
280 |                 .unwrap()
281 |                 .clone(),
282 |             },
283 |         ],
284 |     })
285 | }
286 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/github/branches.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"encoding/json"
  5 | 	"fmt"
  6 | 	"strings"
  7 | 
  8 | 	"github.com/extism/go-pdk"
  9 | )
 10 | 
 11 | var (
 12 | 	CreateBranchTool = ToolDescription{
 13 | 		Name:        "gh-create-branch",
 14 | 		Description: "Create a branch in a GitHub repository",
 15 | 		InputSchema: schema{
 16 | 			"type": "object",
 17 | 			"properties": props{
 18 | 				"owner":       prop("string", "The owner of the repository"),
 19 | 				"repo":        prop("string", "The repository name"),
 20 | 				"branch":      prop("string", "The branch name"),
 21 | 				"from_branch": prop("string", "Source branch (defaults to `main` if not provided)"),
 22 | 			},
 23 | 			"required": []string{"owner", "repo", "branch", "from_branch"},
 24 | 		},
 25 | 	}
 26 | 	ListPullRequestsTool = ToolDescription{
 27 | 		Name:        "gh-list-pull-requests",
 28 | 		Description: "Lists pull requests in a specified repository. Supports different response formats via accept parameter.",
 29 | 		InputSchema: schema{
 30 | 			"type": "object",
 31 | 			"properties": props{
 32 | 				"owner":     prop("string", "The account owner of the repository. The name is not case sensitive."),
 33 | 				"repo":      prop("string", "The name of the repository without the .git extension. The name is not case sensitive."),
 34 | 				"state":     prop("string", "Either open, closed, or all to filter by state."),
 35 | 				"head":      prop("string", "Filter pulls by head user or head organization and branch name in the format of user:ref-name or organization:ref-name."),
 36 | 				"base":      prop("string", "Filter pulls by base branch name. Example: gh-pages"),
 37 | 				"sort":      prop("string", "What to sort results by. Can be one of: created, updated, popularity, long-running"),
 38 | 				"direction": prop("string", "The direction of the sort. Default: desc when sort is created or not specified, otherwise asc"),
 39 | 				"per_page":  prop("integer", "The number of results per page (max 100)"),
 40 | 				"page":      prop("integer", "The page number of the results to fetch"),
 41 | 				"accept":    prop("string", "Response format: raw (default), text, html, or full. Raw returns body, text returns body_text, html returns body_html, full returns all."),
 42 | 			},
 43 | 			"required": []string{"owner", "repo"},
 44 | 		},
 45 | 	}
 46 | 	CreatePullRequestTool = ToolDescription{
 47 | 		Name:        "gh-create-pull-request",
 48 | 		Description: "Create a pull request in a GitHub repository",
 49 | 		InputSchema: schema{
 50 | 			"type": "object",
 51 | 			"properties": props{
 52 | 				"owner":                 prop("string", "The owner of the repository"),
 53 | 				"repo":                  prop("string", "The repository name"),
 54 | 				"title":                 prop("string", "The title of the pull request"),
 55 | 				"body":                  prop("string", "The body of the pull request"),
 56 | 				"head":                  prop("string", "The branch you want to merge into the base branch"),
 57 | 				"base":                  prop("string", "The branch you want to merge into"),
 58 | 				"draft":                 prop("boolean", "Create as draft (optional)"),
 59 | 				"maintainer_can_modify": prop("boolean", "Allow maintainers to modify the pull request"),
 60 | 			},
 61 | 			"required": []string{"owner", "repo", "title", "body", "head", "base"},
 62 | 		},
 63 | 	}
 64 | )
 65 | 
 66 | var BranchTools = []ToolDescription{
 67 | 	CreateBranchTool,
 68 | 	ListPullRequestsTool,
 69 | 	CreatePullRequestTool,
 70 | }
 71 | 
 72 | type RefObjectSchema struct {
 73 | 	Sha  string `json:"sha"`
 74 | 	Type string `json:"type"`
 75 | 	URL  string `json:"url"`
 76 | }
 77 | type RefSchema struct {
 78 | 	Ref    string          `json:"ref"`
 79 | 	NodeID string          `json:"node_id"`
 80 | 	URL    string          `json:"url"`
 81 | 	Object RefObjectSchema `json:"object"`
 82 | }
 83 | 
 84 | func branchCreate(apiKey, owner, repo, branch string, fromBranch *string) CallToolResult {
 85 | 	from := "main"
 86 | 	if fromBranch != nil {
 87 | 		from = *fromBranch
 88 | 	}
 89 | 	sha, err := branchGetSha(apiKey, owner, repo, from)
 90 | 	if err != nil {
 91 | 		return CallToolResult{
 92 | 			IsError: some(true),
 93 | 			Content: []Content{{
 94 | 				Type: ContentTypeText,
 95 | 				Text: some(fmt.Sprintf("Failed to get sha for branch %s: %s", from, err)),
 96 | 			}},
 97 | 		}
 98 | 	}
 99 | 
100 | 	url := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/refs", owner, repo)
101 | 	req := pdk.NewHTTPRequest(pdk.MethodPost, url)
102 | 	req.SetHeader("Authorization", fmt.Sprintf("token %s", apiKey))
103 | 	req.SetHeader("Content-Type", "application/json")
104 | 	req.SetHeader("Accept", "application/vnd.github.v3+json")
105 | 	req.SetHeader("User-Agent", "github-mcpx-servlet")
106 | 
107 | 	data := map[string]interface{}{
108 | 		"ref": fmt.Sprintf("refs/heads/%s", branch),
109 | 		"sha": sha,
110 | 	}
111 | 	res, err := json.Marshal(data)
112 | 	if err != nil {
113 | 		return CallToolResult{
114 | 			IsError: some(true),
115 | 			Content: []Content{{
116 | 				Type: ContentTypeText,
117 | 				Text: some(fmt.Sprintf("Failed to marshal branch data: %s", err)),
118 | 			}},
119 | 		}
120 | 	}
121 | 
122 | 	req.SetBody([]byte(res))
123 | 	resp := req.Send()
124 | 	if resp.Status() != 201 {
125 | 		return CallToolResult{
126 | 			IsError: some(true),
127 | 			Content: []Content{{
128 | 				Type: ContentTypeText,
129 | 				Text: some(fmt.Sprintf("Failed to create branch: %d %s", resp.Status(), string(resp.Body()))),
130 | 			}},
131 | 		}
132 | 	}
133 | 
134 | 	return CallToolResult{
135 | 		Content: []Content{{
136 | 			Type: ContentTypeText,
137 | 			Text: some(string(resp.Body())),
138 | 		}},
139 | 	}
140 | }
141 | 
142 | type PullRequestSchema struct {
143 | 	Title               string `json:"title"`
144 | 	Body                string `json:"body"`
145 | 	Head                string `json:"head"`
146 | 	Base                string `json:"base"`
147 | 	Draft               bool   `json:"draft"`
148 | 	MaintainerCanModify bool   `json:"maintainer_can_modify"`
149 | }
150 | 
151 | func branchPullRequestSchemaFromArgs(args map[string]interface{}) PullRequestSchema {
152 | 	prs := PullRequestSchema{
153 | 		Title: args["title"].(string),
154 | 		Body:  args["body"].(string),
155 | 		Head:  args["head"].(string),
156 | 		Base:  args["base"].(string),
157 | 	}
158 | 	if draft, ok := args["draft"].(bool); ok {
159 | 		prs.Draft = draft
160 | 	}
161 | 	if canModify, ok := args["maintainer_can_modify"].(bool); ok {
162 | 		prs.MaintainerCanModify = canModify
163 | 	}
164 | 	return prs
165 | }
166 | 
167 | func pullRequestList(apiKey string, owner, repo string, args map[string]interface{}) (CallToolResult, error) {
168 | 	baseURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/pulls", owner, repo)
169 | 	params := make([]string, 0)
170 | 
171 | 	// Handle state parameter
172 | 	if state, ok := args["state"].(string); ok && state != "" {
173 | 		switch state {
174 | 		case "open", "closed", "all":
175 | 			params = append(params, fmt.Sprintf("state=%s", state))
176 | 		}
177 | 	} else {
178 | 		params = append(params, "state=open") // Default value
179 | 	}
180 | 
181 | 	// Handle head parameter (user:ref-name or organization:ref-name format)
182 | 	if head, ok := args["head"].(string); ok && head != "" {
183 | 		params = append(params, fmt.Sprintf("head=%s", head))
184 | 	}
185 | 
186 | 	// Handle base parameter
187 | 	if base, ok := args["base"].(string); ok && base != "" {
188 | 		params = append(params, fmt.Sprintf("base=%s", base))
189 | 	}
190 | 
191 | 	// Handle sort parameter
192 | 	sort := "created" // Default value
193 | 	if sortArg, ok := args["sort"].(string); ok && sortArg != "" {
194 | 		switch sortArg {
195 | 		case "created", "updated", "popularity", "long-running":
196 | 			sort = sortArg
197 | 		}
198 | 	}
199 | 	params = append(params, fmt.Sprintf("sort=%s", sort))
200 | 
201 | 	// Handle direction parameter
202 | 	direction := "desc" // Default for created or unspecified sort
203 | 	if sort != "created" {
204 | 		direction = "asc" // Default for other sort types
205 | 	}
206 | 	if dirArg, ok := args["direction"].(string); ok {
207 | 		switch dirArg {
208 | 		case "asc", "desc":
209 | 			direction = dirArg
210 | 		}
211 | 	}
212 | 	params = append(params, fmt.Sprintf("direction=%s", direction))
213 | 
214 | 	// Handle pagination
215 | 	perPage := 30 // Default value
216 | 	if perPageArg, ok := args["per_page"].(float64); ok {
217 | 		if perPageArg > 100 {
218 | 			perPage = 100 // Max value
219 | 		} else if perPageArg > 0 {
220 | 			perPage = int(perPageArg)
221 | 		}
222 | 	}
223 | 	params = append(params, fmt.Sprintf("per_page=%d", perPage))
224 | 
225 | 	page := 1 // Default value
226 | 	if pageArg, ok := args["page"].(float64); ok && pageArg > 0 {
227 | 		page = int(pageArg)
228 | 	}
229 | 	params = append(params, fmt.Sprintf("page=%d", page))
230 | 
231 | 	// Build final URL
232 | 	url := fmt.Sprintf("%s?%s", baseURL, strings.Join(params, "&"))
233 | 	pdk.Log(pdk.LogDebug, fmt.Sprint("Listing pull requests: ", url))
234 | 
235 | 	// Make request
236 | 	req := pdk.NewHTTPRequest(pdk.MethodGet, url)
237 | 	req.SetHeader("Authorization", fmt.Sprint("token ", apiKey))
238 | 
239 | 	// Handle Accept header based on requested format
240 | 	acceptHeader := "application/vnd.github+json" // Default recommended header
241 | 	if format, ok := args["accept"].(string); ok {
242 | 		switch format {
243 | 		case "raw":
244 | 			acceptHeader = "application/vnd.github.raw+json"
245 | 		case "text":
246 | 			acceptHeader = "application/vnd.github.text+json"
247 | 		case "html":
248 | 			acceptHeader = "application/vnd.github.html+json"
249 | 		case "full":
250 | 			acceptHeader = "application/vnd.github.full+json"
251 | 		}
252 | 	}
253 | 	req.SetHeader("Accept", acceptHeader)
254 | 	req.SetHeader("User-Agent", "github-mcpx-servlet")
255 | 
256 | 	resp := req.Send()
257 | 
258 | 	// Handle response status codes
259 | 	switch resp.Status() {
260 | 	case 200:
261 | 		return CallToolResult{
262 | 			Content: []Content{{
263 | 				Type: ContentTypeText,
264 | 				Text: some(string(resp.Body())),
265 | 			}},
266 | 		}, nil
267 | 	case 304:
268 | 		return CallToolResult{
269 | 			IsError: some(true),
270 | 			Content: []Content{{
271 | 				Type: ContentTypeText,
272 | 				Text: some("Not modified"),
273 | 			}},
274 | 		}, nil
275 | 	case 422:
276 | 		return CallToolResult{
277 | 			IsError: some(true),
278 | 			Content: []Content{{
279 | 				Type: ContentTypeText,
280 | 				Text: some("Validation failed, or the endpoint has been spammed."),
281 | 			}},
282 | 		}, nil
283 | 	default:
284 | 		return CallToolResult{
285 | 			IsError: some(true),
286 | 			Content: []Content{{
287 | 				Type: ContentTypeText,
288 | 				Text: some(fmt.Sprintf("Request failed with status %d: %s", resp.Status(), string(resp.Body()))),
289 | 			}},
290 | 		}, nil
291 | 	}
292 | }
293 | 
294 | func branchCreatePullRequest(apiKey, owner, repo string, pr PullRequestSchema) CallToolResult {
295 | 	url := fmt.Sprintf("https://api.github.com/repos/%s/%s/pulls", owner, repo)
296 | 	req := pdk.NewHTTPRequest(pdk.MethodPost, url)
297 | 	req.SetHeader("Authorization", fmt.Sprintf("token %s", apiKey))
298 | 	req.SetHeader("Accept", "application/vnd.github.v3+json")
299 | 	req.SetHeader("User-Agent", "github-mcpx-servlet")
300 | 	req.SetHeader("Content-Type", "application/json")
301 | 
302 | 	res, err := json.Marshal(pr)
303 | 	if err != nil {
304 | 		return CallToolResult{
305 | 			IsError: some(true),
306 | 			Content: []Content{{
307 | 				Type: ContentTypeText,
308 | 				Text: some(fmt.Sprintf("Failed to marshal pull request data: %s", err)),
309 | 			}},
310 | 		}
311 | 	}
312 | 
313 | 	req.SetBody([]byte(res))
314 | 	resp := req.Send()
315 | 	if resp.Status() != 201 {
316 | 		return CallToolResult{
317 | 			IsError: some(true),
318 | 			Content: []Content{{
319 | 				Type: ContentTypeText,
320 | 				Text: some(fmt.Sprintf("Failed to create pull request: %d %s", resp.Status(), string(resp.Body()))),
321 | 			}},
322 | 		}
323 | 	}
324 | 
325 | 	return CallToolResult{
326 | 		Content: []Content{{
327 | 			Type: ContentTypeText,
328 | 			Text: some(string(resp.Body())),
329 | 		}},
330 | 	}
331 | }
332 | 
333 | func branchGetSha(apiKey, owner, repo, ref string) (string, error) {
334 | 	url := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/refs/heads/%s", owner, repo, ref)
335 | 	req := pdk.NewHTTPRequest(pdk.MethodGet, url)
336 | 	req.SetHeader("Authorization", fmt.Sprintf("token %s", apiKey))
337 | 	req.SetHeader("Accept", "application/vnd.github.v3+json")
338 | 	req.SetHeader("User-Agent", "github-mcpx-servlet")
339 | 
340 | 	resp := req.Send()
341 | 	if resp.Status() != 200 {
342 | 		return "", fmt.Errorf("Failed to get main branch sha: %d", resp.Status())
343 | 	}
344 | 
345 | 	var refDetail RefSchema
346 | 	json.Unmarshal(resp.Body(), &refDetail)
347 | 	return refDetail.Object.Sha, nil
348 | }
349 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/github/issues.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"encoding/json"
  5 | 	"fmt"
  6 | 	"strings"
  7 | 
  8 | 	"github.com/extism/go-pdk"
  9 | )
 10 | 
 11 | var (
 12 | 	ListIssuesTool = ToolDescription{
 13 | 		Name:        "gh-list-issues",
 14 | 		Description: "List issues from a GitHub repository",
 15 | 		InputSchema: schema{
 16 | 			"type": "object",
 17 | 			"properties": props{
 18 | 				"owner":     prop("string", "The owner of the repository"),
 19 | 				"repo":      prop("string", "The repository name"),
 20 | 				"filter":    prop("string", "Filter by assigned, created, mentioned, subscribed, repos, all"),
 21 | 				"state":     prop("string", "The state of the issues (open, closed, all)"),
 22 | 				"labels":    prop("string", "A list of comma separated label names (e.g. bug,ui,@high)"),
 23 | 				"sort":      prop("string", "Sort field (created, updated, comments)"),
 24 | 				"direction": prop("string", "Sort direction (asc or desc)"),
 25 | 				"since":     prop("string", "ISO 8601 timestamp (YYYY-MM-DDTHH:MM:SSZ)"),
 26 | 				"collab":    prop("boolean", "Filter by issues that are collaborated on"),
 27 | 				"orgs":      prop("boolean", "Filter by organization issues"),
 28 | 				"owned":     prop("boolean", "Filter by owned issues"),
 29 | 				"pulls":     prop("boolean", "Include pull requests in results"),
 30 | 				"per_page":  prop("integer", "Number of results per page (max 100)"),
 31 | 				"page":      prop("integer", "Page number for pagination"),
 32 | 			},
 33 | 			"required": []string{"owner", "repo"},
 34 | 		},
 35 | 	}
 36 | 	CreateIssueTool = ToolDescription{
 37 | 		Name:        "gh-create-issue",
 38 | 		Description: "Create an issue on a GitHub repository",
 39 | 		InputSchema: schema{
 40 | 			"type": "object",
 41 | 			"properties": props{
 42 | 				"owner":     prop("string", "The owner of the repository"),
 43 | 				"repo":      prop("string", "The repository name"),
 44 | 				"title":     prop("string", "The title of the issue"),
 45 | 				"body":      prop("string", "The body of the issue"),
 46 | 				"state":     prop("string", "The state of the issue"),
 47 | 				"assignees": arrprop("array", "The assignees of the issue", "string"),
 48 | 				"milestone": prop("integer", "The milestone of the issue"),
 49 | 			},
 50 | 			"required": []string{"owner", "repo", "title", "body"},
 51 | 		},
 52 | 	}
 53 | 	GetIssueTool = ToolDescription{
 54 | 		Name:        "gh-get-issue",
 55 | 		Description: "Get an issue from a GitHub repository",
 56 | 		InputSchema: schema{
 57 | 			"type": "object",
 58 | 			"properties": props{
 59 | 				"owner": prop("string", "The owner of the repository"),
 60 | 				"repo":  prop("string", "The repository name"),
 61 | 				"issue": prop("integer", "The issue number"),
 62 | 			},
 63 | 			"required": []string{"owner", "repo", "issue"},
 64 | 		},
 65 | 	}
 66 | 	AddIssueCommentTool = ToolDescription{
 67 | 		Name:        "gh-add-issue-comment",
 68 | 		Description: "Add a comment to an issue in a GitHub repository",
 69 | 		InputSchema: schema{
 70 | 			"type": "object",
 71 | 			"properties": props{
 72 | 				"owner": prop("string", "The owner of the repository"),
 73 | 				"repo":  prop("string", "The repository name"),
 74 | 				"issue": prop("integer", "The issue number"),
 75 | 				"body":  prop("string", "The body of the issue"),
 76 | 			},
 77 | 			"required": []string{"owner", "repo", "issue", "body"},
 78 | 		},
 79 | 	}
 80 | 	UpdateIssueTool = ToolDescription{
 81 | 		Name:        "gh-update-issue",
 82 | 		Description: "Update an issue in a GitHub repository",
 83 | 		InputSchema: schema{
 84 | 			"type": "object",
 85 | 			"properties": props{
 86 | 				"owner":     prop("string", "The owner of the repository"),
 87 | 				"repo":      prop("string", "The repository name"),
 88 | 				"issue":     prop("integer", "The issue number"),
 89 | 				"title":     prop("string", "The title of the issue"),
 90 | 				"body":      prop("string", "The body of the issue"),
 91 | 				"state":     prop("string", "The state of the issue"),
 92 | 				"assignees": arrprop("array", "The assignees of the issue", "string"),
 93 | 				"milestone": prop("integer", "The milestone of the issue"),
 94 | 			},
 95 | 			"required": []string{"owner", "repo", "issue"},
 96 | 		},
 97 | 	}
 98 | 	IssueTools = []ToolDescription{
 99 | 		ListIssuesTool,
100 | 		CreateIssueTool,
101 | 		GetIssueTool,
102 | 		UpdateIssueTool,
103 | 		AddIssueCommentTool,
104 | 	}
105 | )
106 | 
107 | type Issue struct {
108 | 	Title     string   `json:"title,omitempty"`
109 | 	Body      string   `json:"body,omitempty"`
110 | 	Assignees []string `json:"assignees,omitempty"`
111 | 	Milestone int      `json:"milestone,omitempty"`
112 | 	Labels    []string `json:"labels,omitempty"`
113 | }
114 | 
115 | func issueList(apiKey string, owner, repo string, args map[string]interface{}) (CallToolResult, error) {
116 | 	baseURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/issues", owner, repo)
117 | 	params := make([]string, 0)
118 | 
119 | 	// String parameters
120 | 	stringParams := map[string]string{
121 | 		"filter":    "assigned", // Default value
122 | 		"state":     "open",     // Default value
123 | 		"labels":    "",
124 | 		"sort":      "created", // Default value
125 | 		"direction": "desc",    // Default value
126 | 		"since":     "",
127 | 	}
128 | 
129 | 	for key := range stringParams {
130 | 		if value, ok := args[key].(string); ok && value != "" {
131 | 			params = append(params, fmt.Sprintf("%s=%s", key, value))
132 | 		} else if stringParams[key] != "" {
133 | 			// Add default value if one exists
134 | 			params = append(params, fmt.Sprintf("%s=%s", key, stringParams[key]))
135 | 		}
136 | 	}
137 | 
138 | 	// Boolean parameters
139 | 	boolParams := []string{"collab", "orgs", "owned", "pulls"}
140 | 	for _, param := range boolParams {
141 | 		if value, ok := args[param].(bool); ok {
142 | 			params = append(params, fmt.Sprintf("%s=%t", param, value))
143 | 		}
144 | 	}
145 | 
146 | 	// Pagination parameters
147 | 	perPage := 30 // Default value
148 | 	if value, ok := args["per_page"].(float64); ok {
149 | 		if value > 100 {
150 | 			perPage = 100 // Max value
151 | 		} else if value > 0 {
152 | 			perPage = int(value)
153 | 		}
154 | 	}
155 | 	params = append(params, fmt.Sprintf("per_page=%d", perPage))
156 | 
157 | 	page := 1 // Default value
158 | 	if value, ok := args["page"].(float64); ok && value > 0 {
159 | 		page = int(value)
160 | 	}
161 | 	params = append(params, fmt.Sprintf("page=%d", page))
162 | 
163 | 	// Build final URL
164 | 	url := baseURL
165 | 	if len(params) > 0 {
166 | 		url = fmt.Sprintf("%s?%s", baseURL, strings.Join(params, "&"))
167 | 	}
168 | 
169 | 	pdk.Log(pdk.LogDebug, fmt.Sprint("Listing issues: ", url))
170 | 
171 | 	// Make request
172 | 	req := pdk.NewHTTPRequest(pdk.MethodGet, url)
173 | 	req.SetHeader("Authorization", fmt.Sprint("token ", apiKey))
174 | 	req.SetHeader("Accept", "application/vnd.github+json")
175 | 	req.SetHeader("User-Agent", "github-mcpx-servlet")
176 | 
177 | 	resp := req.Send()
178 | 	if resp.Status() != 200 {
179 | 		return CallToolResult{
180 | 			IsError: some(true),
181 | 			Content: []Content{{
182 | 				Type: ContentTypeText,
183 | 				Text: some(fmt.Sprintf("Failed to list issues: %d %s", resp.Status(), string(resp.Body()))),
184 | 			}},
185 | 		}, nil
186 | 	}
187 | 
188 | 	return CallToolResult{
189 | 		Content: []Content{{
190 | 			Type: ContentTypeText,
191 | 			Text: some(string(resp.Body())),
192 | 		}},
193 | 	}, nil
194 | }
195 | 
196 | func issueFromArgs(args map[string]interface{}) Issue {
197 | 	data := Issue{}
198 | 	if title, ok := args["title"].(string); ok {
199 | 		data.Title = title
200 | 	}
201 | 	if body, ok := args["body"].(string); ok {
202 | 		data.Body = body
203 | 	}
204 | 	if assignees, ok := args["assignees"].([]interface{}); ok {
205 | 		for _, a := range assignees {
206 | 			data.Assignees = append(data.Assignees, a.(string))
207 | 		}
208 | 	}
209 | 	if milestone, ok := args["milestone"].(float64); ok {
210 | 		data.Milestone = int(milestone)
211 | 	}
212 | 	if labels, ok := args["labels"].([]interface{}); ok {
213 | 		for _, l := range labels {
214 | 			data.Labels = append(data.Labels, l.(string))
215 | 		}
216 | 	}
217 | 	return data
218 | }
219 | 
220 | func issueCreate(apiKey string, owner, repo string, data Issue) (CallToolResult, error) {
221 | 	url := fmt.Sprint("https://api.github.com/repos/", owner, "/", repo, "/issues")
222 | 	pdk.Log(pdk.LogDebug, fmt.Sprint("Adding comment: ", url))
223 | 
224 | 	req := pdk.NewHTTPRequest(pdk.MethodPost, url)
225 | 	req.SetHeader("Authorization", fmt.Sprint("token ", apiKey))
226 | 	req.SetHeader("Accept", "application/vnd.github.v3+json")
227 | 	req.SetHeader("User-Agent", "github-mcpx-servlet")
228 | 	req.SetHeader("Content-Type", "application/json")
229 | 
230 | 	res, err := json.Marshal(data)
231 | 
232 | 	if err != nil {
233 | 		return CallToolResult{
234 | 			IsError: some(true),
235 | 			Content: []Content{{
236 | 				Type: ContentTypeText,
237 | 				Text: some(fmt.Sprint("Failed to create issue: ", err)),
238 | 			}},
239 | 		}, nil
240 | 	}
241 | 
242 | 	req.SetBody([]byte(res))
243 | 	resp := req.Send()
244 | 
245 | 	if resp.Status() != 201 {
246 | 		return CallToolResult{
247 | 			IsError: some(true),
248 | 			Content: []Content{{
249 | 				Type: ContentTypeText,
250 | 				Text: some(fmt.Sprint("Failed to create issue: ", resp.Status(), " ", string(resp.Body()))),
251 | 			}},
252 | 		}, nil
253 | 	}
254 | 
255 | 	return CallToolResult{
256 | 		Content: []Content{{
257 | 			Type: ContentTypeText,
258 | 			Text: some(string(resp.Body())),
259 | 		}},
260 | 	}, nil
261 | }
262 | 
263 | func issueGet(apiKey string, owner, repo string, issue int) (CallToolResult, error) {
264 | 	url := fmt.Sprint("https://api.github.com/repos/", owner, "/", repo, "/issues/", issue)
265 | 	pdk.Log(pdk.LogDebug, fmt.Sprint("Getting issue: ", url))
266 | 
267 | 	req := pdk.NewHTTPRequest(pdk.MethodGet, url)
268 | 	req.SetHeader("Authorization", fmt.Sprint("token ", apiKey))
269 | 	req.SetHeader("Accept", "application/vnd.github.v3+json")
270 | 	req.SetHeader("User-Agent", "github-mcpx-servlet")
271 | 	resp := req.Send()
272 | 	if resp.Status() != 200 {
273 | 		return CallToolResult{
274 | 			IsError: some(true),
275 | 			Content: []Content{{
276 | 				Type: ContentTypeText,
277 | 				Text: some(fmt.Sprint("Failed to get issue: ", resp.Status())),
278 | 			}},
279 | 		}, nil
280 | 	}
281 | 
282 | 	return CallToolResult{
283 | 		Content: []Content{{
284 | 			Type: ContentTypeText,
285 | 			Text: some(string(resp.Body())),
286 | 		}},
287 | 	}, nil
288 | }
289 | 
290 | func issueUpdate(apiKey string, owner, repo string, issue int, data Issue) (CallToolResult, error) {
291 | 	url := fmt.Sprint("https://api.github.com/repos/", owner, "/", repo, "/issues/", issue)
292 | 	pdk.Log(pdk.LogDebug, fmt.Sprint("Getting issue: ", url))
293 | 
294 | 	req := pdk.NewHTTPRequest(pdk.MethodPatch, url)
295 | 	req.SetHeader("Authorization", fmt.Sprint("token ", apiKey))
296 | 	req.SetHeader("Accept", "application/vnd.github.v3+json")
297 | 	req.SetHeader("User-Agent", "github-mcpx-servlet")
298 | 	req.SetHeader("Content-Type", "application/json")
299 | 
300 | 	res, err := json.Marshal(data)
301 | 	if err != nil {
302 | 		return CallToolResult{
303 | 			IsError: some(true),
304 | 			Content: []Content{{
305 | 				Type: ContentTypeText,
306 | 				Text: some(fmt.Sprint("Failed to update issue: ", err)),
307 | 			}},
308 | 		}, nil
309 | 	}
310 | 
311 | 	req.SetBody([]byte(res))
312 | 	resp := req.Send()
313 | 	if resp.Status() != 200 {
314 | 		return CallToolResult{
315 | 			IsError: some(true),
316 | 			Content: []Content{{
317 | 				Type: ContentTypeText,
318 | 				Text: some(fmt.Sprint("Failed to update issue: ", resp.Status())),
319 | 			}},
320 | 		}, nil
321 | 	}
322 | 
323 | 	return CallToolResult{
324 | 		Content: []Content{{
325 | 			Type: ContentTypeText,
326 | 			Text: some(string(resp.Body())),
327 | 		}},
328 | 	}, nil
329 | }
330 | 
331 | func issueAddComment(apiKey string, owner, repo string, issue int, comment string) (CallToolResult, error) {
332 | 	url := fmt.Sprint("https://api.github.com/repos/", owner, "/", repo, "/issues/", issue, "/comments")
333 | 	pdk.Log(pdk.LogDebug, fmt.Sprint("Adding comment: ", url))
334 | 
335 | 	req := pdk.NewHTTPRequest(pdk.MethodPost, url)
336 | 	req.SetHeader("Authorization", fmt.Sprint("token ", apiKey))
337 | 	req.SetHeader("Accept", "application/vnd.github.v3+json")
338 | 	req.SetHeader("User-Agent", "github-mcpx-servlet")
339 | 	req.SetHeader("Content-Type", "application/json")
340 | 
341 | 	res, err := json.Marshal(map[string]string{
342 | 		"body": comment,
343 | 	})
344 | 
345 | 	if err != nil {
346 | 		return CallToolResult{
347 | 			IsError: some(true),
348 | 			Content: []Content{{
349 | 				Type: ContentTypeText,
350 | 				Text: some(fmt.Sprint("Failed to create issue: ", err)),
351 | 			}},
352 | 		}, nil
353 | 	}
354 | 
355 | 	req.SetBody([]byte(res))
356 | 	resp := req.Send()
357 | 
358 | 	if resp.Status() != 201 {
359 | 		return CallToolResult{
360 | 			IsError: some(true),
361 | 			Content: []Content{{
362 | 				Type: ContentTypeText,
363 | 				Text: some(fmt.Sprint("Failed to add comment: ", resp.Status())),
364 | 			}},
365 | 		}, nil
366 | 	}
367 | 
368 | 	return CallToolResult{
369 | 		Content: []Content{{
370 | 			Type: ContentTypeText,
371 | 			Text: some(string(resp.Body())),
372 | 		}},
373 | 	}, nil
374 | }
375 | 
```

--------------------------------------------------------------------------------
/SKIP_TOOLS_GUIDE.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Skip Tools Pattern Guide
  2 | 
  3 | This guide provides comprehensive documentation for using the `skip_tools` configuration in hyper-mcp, which allows you to filter out unwanted tools using powerful regex patterns.
  4 | 
  5 | ## Overview
  6 | 
  7 | The `skip_tools` field in your plugin's `runtime_config` allows you to specify a list of regex patterns that will be used to exclude tools from being loaded at runtime. This is useful for:
  8 | 
  9 | - Removing debug tools in production environments
 10 | - Filtering out deprecated or experimental tools
 11 | - Excluding tools that conflict with your workflow
 12 | - Customizing the available tool set per environment
 13 | 
 14 | ## How It Works
 15 | 
 16 | ### Automatic Pattern Anchoring
 17 | 
 18 | All patterns in `skip_tools` are automatically anchored to match the entire tool name. This means:
 19 | 
 20 | ```yaml
 21 | skip_tools:
 22 |   - "debug"  # Becomes "^debug$" - matches exactly "debug"
 23 | ```
 24 | 
 25 | This prevents unintended partial matches. If you want to match parts of tool names, use explicit wildcards:
 26 | 
 27 | ```yaml
 28 | skip_tools:
 29 |   - "debug.*"  # Matches "debug", "debugger", "debug_info", etc.
 30 | ```
 31 | 
 32 | ### Regex Compilation
 33 | 
 34 | All patterns are compiled into a single optimized `RegexSet` for efficient matching:
 35 | - O(1) lookup time regardless of pattern count
 36 | - Single compilation at startup
 37 | - Memory-efficient pattern storage
 38 | 
 39 | ## Basic Patterns
 40 | 
 41 | ### Exact Matches
 42 | 
 43 | Match specific tool names exactly:
 44 | 
 45 | ```yaml
 46 | skip_tools:
 47 |   - "debug_tool"      # Matches only "debug_tool"
 48 |   - "test_runner"     # Matches only "test_runner"
 49 |   - "admin_panel"     # Matches only "admin_panel"
 50 | ```
 51 | 
 52 | ### Prefix Matching
 53 | 
 54 | Match tools that start with a specific string:
 55 | 
 56 | ```yaml
 57 | skip_tools:
 58 |   - "debug.*"         # Matches "debug", "debugger", "debug_info"
 59 |   - "test_.*"         # Matches "test_unit", "test_integration", "test_e2e"
 60 |   - "dev_.*"          # Matches "dev_server", "dev_tools", "dev_helper"
 61 | ```
 62 | 
 63 | ### Suffix Matching
 64 | 
 65 | Match tools that end with a specific string:
 66 | 
 67 | ```yaml
 68 | skip_tools:
 69 |   - ".*_test"         # Matches "unit_test", "integration_test", "load_test"
 70 |   - ".*_backup"       # Matches "data_backup", "config_backup", "db_backup"
 71 |   - ".*_deprecated"   # Matches "old_deprecated", "legacy_deprecated"
 72 | ```
 73 | 
 74 | ### Contains Matching
 75 | 
 76 | Match tools that contain a specific substring:
 77 | 
 78 | ```yaml
 79 | skip_tools:
 80 |   - ".*debug.*"       # Matches "pre_debug_tool", "debug", "tool_debug_info"
 81 |   - ".*temp.*"        # Matches "temp_file", "cleanup_temp", "temp_storage_tool"
 82 | ```
 83 | 
 84 | ## Advanced Patterns
 85 | 
 86 | ### Character Classes
 87 | 
 88 | Use character classes for flexible matching:
 89 | 
 90 | ```yaml
 91 | skip_tools:
 92 |   - "tool_[0-9]+"           # Matches "tool_1", "tool_42", "tool_999"
 93 |   - "test_[a-z]+"           # Matches "test_unit", "test_api", "test_db"
 94 |   - "[A-Z][a-z]+Tool"       # Matches "DebugTool", "TestTool", "AdminTool"
 95 |   - "log_[0-9]{4}_[0-9]{2}" # Matches "log_2023_12", "log_2024_01"
 96 | ```
 97 | 
 98 | ### Alternation (OR Logic)
 99 | 
100 | Match multiple alternatives:
101 | 
102 | ```yaml
103 | skip_tools:
104 |   - "test_(unit|integration|e2e)"     # Matches "test_unit", "test_integration", "test_e2e"
105 |   - "(debug|trace|log)_.*"            # Matches tools starting with "debug_", "trace_", or "log_"
106 |   - ".*(temp|tmp|cache).*"            # Matches tools containing "temp", "tmp", or "cache"
107 |   - "system_(admin|user|guest)_.*"    # Matches tools for different user types
108 | ```
109 | 
110 | ### Quantifiers
111 | 
112 | Control how many characters or groups to match:
113 | 
114 | ```yaml
115 | skip_tools:
116 |   - "tool_v[0-9]+"          # Matches "tool_v1", "tool_v10", "tool_v123"
117 |   - "backup_[0-9]{8}"       # Matches exactly 8 digits: "backup_20240101"
118 |   - "temp_[a-f0-9]{6,}"     # Matches 6+ hex chars: "temp_abc123", "temp_def456789"
119 |   - "log_[0-9]{4}-[0-9]{2}" # Matches "log_2024-01", "log_2023-12"
120 | ```
121 | 
122 | ### Negation with Character Classes
123 | 
124 | Skip tools that DON'T match certain patterns:
125 | 
126 | ```yaml
127 | skip_tools:
128 |   - "[^a-z].*"              # Skip tools starting with non-lowercase letters
129 |   - ".*[^0-9]$"             # Skip tools not ending with numbers
130 |   - "tool_[^v].*"           # Skip tools starting with "tool_" but not "tool_v"
131 | ```
132 | 
133 | ## Common Use Cases
134 | 
135 | ### Environment-Specific Filtering
136 | 
137 | #### Development Environment
138 | ```yaml
139 | skip_tools:
140 |   - "prod_.*"               # Skip production tools
141 |   - "deploy_.*"             # Skip deployment tools
142 |   - "monitor_.*"            # Skip monitoring tools
143 | ```
144 | 
145 | #### Production Environment
146 | ```yaml
147 | skip_tools:
148 |   - "debug.*"               # Skip all debug tools
149 |   - "test_.*"               # Skip all test tools
150 |   - "dev_.*"                # Skip development tools
151 |   - "mock_.*"               # Skip mock/stub tools
152 |   - ".*_experimental"       # Skip experimental features
153 | ```
154 | 
155 | #### Testing Environment
156 | ```yaml
157 | skip_tools:
158 |   - "prod_.*"               # Skip production tools
159 |   - "deploy_.*"             # Skip deployment tools
160 |   - ".*_live"               # Skip live/production tools
161 | ```
162 | 
163 | ### Tool Category Filtering
164 | 
165 | #### Skip Administrative Tools
166 | ```yaml
167 | skip_tools:
168 |   - "admin_.*"
169 |   - "system_admin_.*"
170 |   - "user_management_.*"
171 |   - "permission_.*"
172 | ```
173 | 
174 | #### Skip Deprecated Tools
175 | ```yaml
176 | skip_tools:
177 |   - ".*_deprecated"
178 |   - ".*_old"
179 |   - "legacy_.*"
180 |   - "v[0-9]_.*"             # Skip versioned legacy tools
181 | ```
182 | 
183 | #### Skip Resource-Heavy Tools
184 | ```yaml
185 | skip_tools:
186 |   - ".*_benchmark"
187 |   - "load_test_.*"
188 |   - "stress_.*"
189 |   - "heavy_.*"
190 | ```
191 | 
192 | ### Version-Based Filtering
193 | 
194 | ```yaml
195 | skip_tools:
196 |   - ".*_v[0-9]"             # Skip v1, v2, etc. (keep latest)
197 |   - ".*_beta"               # Skip beta tools
198 |   - ".*_alpha"              # Skip alpha tools
199 |   - "tool_[0-9]+\\.[0-9]+"  # Skip versioned tools like "tool_1.0"
200 | ```
201 | 
202 | ## Special Character Escaping
203 | 
204 | When matching literal special characters, escape them with backslashes:
205 | 
206 | ```yaml
207 | skip_tools:
208 |   - "file\\.exe"            # Matches "file.exe" literally
209 |   - "script\\?"             # Matches "script?" literally
210 |   - "temp\\*data"           # Matches "temp*data" literally
211 |   - "path\\\\tool"          # Matches "path\tool" literally (double escape for backslash)
212 |   - "price\\$calculator"    # Matches "price$calculator" literally
213 |   - "regex\\[test\\]"       # Matches "regex[test]" literally
214 | ```
215 | 
216 | ## Configuration Examples
217 | 
218 | ### Simple Configuration
219 | ```yaml
220 | plugins:
221 |   my_plugin:
222 |     url: "oci://registry.io/my-plugin:latest"
223 |     runtime_config:
224 |       skip_tools:
225 |         - "debug_tool"
226 |         - "test_runner"
227 | ```
228 | 
229 | ### Comprehensive Configuration
230 | ```yaml
231 | plugins:
232 |   production_plugin:
233 |     url: "oci://registry.io/prod-plugin:latest"
234 |     runtime_config:
235 |       skip_tools:
236 |         # Exact matches
237 |         - "debug_console"
238 |         - "test_runner"
239 | 
240 |         # Pattern matches
241 |         - "dev_.*"              # All dev tools
242 |         - ".*_test"             # All test tools
243 |         - "temp_.*"             # All temp tools
244 |         - "mock_.*"             # All mock tools
245 | 
246 |         # Advanced patterns
247 |         - "tool_v[0-9]"         # Versioned tools
248 |         - "admin_(user|role)_.*" # Specific admin tools
249 |         - "[0-9]+_backup"       # Numbered backups
250 | 
251 |       allowed_hosts: ["api.example.com"]
252 |       memory_limit: "512Mi"
253 | ```
254 | 
255 | ### Multi-Environment Setup
256 | ```yaml
257 | # config.dev.yaml
258 | plugins:
259 |   app_plugin:
260 |     url: "oci://registry.io/app-plugin:dev"
261 |     runtime_config:
262 |       skip_tools:
263 |         - "prod_.*"
264 |         - "deploy_.*"
265 | 
266 | ---
267 | # config.prod.yaml
268 | plugins:
269 |   app_plugin:
270 |     url: "oci://registry.io/app-plugin:latest"
271 |     runtime_config:
272 |       skip_tools:
273 |         - "debug.*"
274 |         - "test_.*"
275 |         - "dev_.*"
276 |         - ".*_experimental"
277 | ```
278 | 
279 | ## Best Practices
280 | 
281 | ### 1. Start Simple, Then Refine
282 | ```yaml
283 | # Start with broad patterns
284 | skip_tools:
285 |   - "debug.*"
286 |   - "test_.*"
287 | 
288 | # Refine to be more specific as needed
289 | skip_tools:
290 |   - "debug_(console|panel)"  # Only skip specific debug tools
291 |   - "test_(unit|integration)" # Only skip specific test types
292 | ```
293 | 
294 | ### 2. Use Comments for Complex Patterns
295 | ```yaml
296 | skip_tools:
297 |   - "tool_[0-9]+"             # Skip numbered tools (tool_1, tool_2, etc.)
298 |   - ".*_(alpha|beta|rc[0-9]+)" # Skip pre-release versions
299 |   - "temp_[0-9]{8}_.*"        # Skip dated temporary tools
300 | ```
301 | 
302 | ### 3. Group Related Patterns
303 | ```yaml
304 | skip_tools:
305 |   # Debug and development tools
306 |   - "debug.*"
307 |   - "dev_.*"
308 |   - ".*_dev"
309 | 
310 |   # Testing tools
311 |   - "test_.*"
312 |   - ".*_test"
313 |   - "mock_.*"
314 | 
315 |   # Administrative tools
316 |   - "admin_.*"
317 |   - "system_.*"
318 | ```
319 | 
320 | ### 4. Consider Performance
321 | ```yaml
322 | # Good: Specific patterns
323 | skip_tools:
324 |   - "debug_tool"
325 |   - "test_runner"
326 | 
327 | # Less optimal: Overly broad patterns that might match many tools
328 | skip_tools:
329 |   - ".*"  # This would skip everything - not useful
330 | ```
331 | 
332 | ## Troubleshooting
333 | 
334 | ### Pattern Not Working?
335 | 
336 | 1. **Check anchoring**: Remember patterns are auto-anchored
337 |    ```yaml
338 |    # This matches only "debug" exactly
339 |    - "debug"
340 | 
341 |    # This matches "debug", "debugger", "debug_tool", etc.
342 |    - "debug.*"
343 |    ```
344 | 
345 | 2. **Escape special characters**:
346 |    ```yaml
347 |    # Wrong: Will treat . as wildcard
348 |    - "file.exe"
349 | 
350 |    # Correct: Escapes the literal dot
351 |    - "file\\.exe"
352 |    ```
353 | 
354 | 3. **Test your patterns**: Use a regex tester to validate complex patterns
355 | 
356 | ### Debugging Skip Rules
357 | 
358 | Enable debug logging to see which tools are being skipped:
359 | 
360 | ```bash
361 | RUST_LOG=debug hyper-mcp --config config.yaml
362 | ```
363 | 
364 | ## Migration from Old Format
365 | 
366 | If you were using simple string arrays before:
367 | 
368 | ```yaml
369 | # Old format (if it existed)
370 | skip_tools: ["debug_tool", "test_runner"]
371 | 
372 | # New format (same result, but now with regex support)
373 | skip_tools: ["debug_tool", "test_runner"]
374 | 
375 | # New format with patterns (more powerful)
376 | skip_tools: ["debug.*", "test_.*"]
377 | ```
378 | 
379 | ## Error Handling
380 | 
381 | ### Invalid Regex Patterns
382 | 
383 | If you provide an invalid regex pattern, configuration loading will fail:
384 | 
385 | ```yaml
386 | # This will cause an error - unclosed bracket
387 | skip_tools:
388 |   - "tool_[invalid"
389 | ```
390 | 
391 | Error message will indicate the problematic pattern and suggest corrections.
392 | 
393 | ### Empty Patterns
394 | 
395 | These configurations are all valid:
396 | 
397 | ```yaml
398 | # No skip_tools field - no tools skipped
399 | runtime_config:
400 |   allowed_hosts: ["*"]
401 | 
402 | # Empty array - no tools skipped
403 | runtime_config:
404 |   skip_tools: []
405 | 
406 | # Null value - no tools skipped
407 | runtime_config:
408 |   skip_tools: null
409 | ```
410 | 
411 | ## Performance Characteristics
412 | 
413 | - **Startup**: O(n) pattern compilation where n = number of patterns
414 | - **Runtime**: O(1) tool name checking regardless of pattern count
415 | - **Memory**: Minimal overhead, patterns compiled into efficient state machine
416 | - **Scalability**: Handles hundreds of patterns efficiently
417 | 
418 | ## Advanced Topics
419 | 
420 | ### Complex Business Logic
421 | 
422 | ```yaml
423 | skip_tools:
424 |   # Skip tools for specific environments
425 |   - "prod_(?!api_).*"         # Skip prod tools except prod_api_*
426 |   - "test_(?!smoke_).*"       # Skip test tools except smoke tests
427 | 
428 |   # Skip based on naming conventions
429 |   - "[A-Z]{2,}_.*"            # Skip tools starting with 2+ capitals
430 |   - ".*_[0-9]{4}[0-9]{2}[0-9]{2}" # Skip daily-dated tools
431 | ```
432 | 
433 | ### Integration with External Tools
434 | 
435 | You can generate `skip_tools` patterns dynamically:
436 | 
437 | ```bash
438 | # Generate patterns from external source
439 | echo "skip_tools:" > config.yaml
440 | external-tool --list-deprecated | sed 's/^/  - "/' | sed 's/$/\"/' >> config.yaml
441 | ```
442 | 
443 | ### Conditional Configuration
444 | 
445 | Use different configs for different scenarios:
446 | 
447 | ```yaml
448 | # Base configuration
449 | base_skip_patterns: &base_skip
450 |   - "debug.*"
451 |   - "test_.*"
452 | 
453 | # Environment-specific additions
454 | prod_additional: &prod_additional
455 |   - "dev_.*"
456 |   - ".*_experimental"
457 | 
458 | plugins:
459 |   my_plugin:
460 |     runtime_config:
461 |       skip_tools:
462 |         - *base_skip
463 |         - *prod_additional  # YAML doesn't support this directly,
464 |                             # but you can use templating tools
465 | ```
466 | 
467 | This guide should help you make full use of the powerful `skip_tools` pattern matching capabilities in hyper-mcp!
468 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/meme-generator/src/lib.rs:
--------------------------------------------------------------------------------

```rust
  1 | mod embedded;
  2 | mod pdk;
  3 | 
  4 | use ab_glyph::{Font, FontArc, PxScale, ScaleFont};
  5 | use base64::Engine;
  6 | use extism_pdk::*;
  7 | use image::Rgba;
  8 | use imageproc::drawing::draw_text_mut;
  9 | use pdk::types::{
 10 |     CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
 11 | };
 12 | use serde::{Deserialize, Serialize};
 13 | use serde_json::json;
 14 | use std::io::Cursor;
 15 | 
 16 | #[derive(Debug, Serialize, Deserialize)]
 17 | struct Example {
 18 |     text: Vec<String>,
 19 |     url: String,
 20 | }
 21 | 
 22 | #[derive(Debug, Serialize, Deserialize)]
 23 | struct MemeTemplate {
 24 |     id: String,
 25 |     name: String,
 26 |     lines: u32,
 27 |     overlays: u32,
 28 |     styles: Vec<String>,
 29 |     blank: String,
 30 |     example: Example,
 31 |     source: Option<String>,
 32 |     keywords: Vec<String>,
 33 |     #[serde(rename = "_self")]
 34 |     self_link: String,
 35 | }
 36 | 
 37 | #[derive(Debug, Serialize, Deserialize)]
 38 | struct TemplateConfig {
 39 |     name: String,
 40 |     source: String,
 41 |     keywords: Vec<String>,
 42 |     text: Vec<TextConfig>,
 43 |     example: Vec<String>,
 44 |     overlay: Vec<OverlayConfig>,
 45 | }
 46 | 
 47 | #[derive(Debug, Serialize, Deserialize)]
 48 | struct TextConfig {
 49 |     style: String,
 50 |     color: String,
 51 |     font: String,
 52 |     anchor_x: f32,
 53 |     anchor_y: f32,
 54 |     angle: f32,
 55 |     scale_x: f32,
 56 |     scale_y: f32,
 57 |     align: String,
 58 |     start: f32,
 59 |     stop: f32,
 60 | }
 61 | 
 62 | #[derive(Debug, Serialize, Deserialize)]
 63 | struct OverlayConfig {
 64 |     center_x: f32,
 65 |     center_y: f32,
 66 |     angle: f32,
 67 |     scale: f32,
 68 | }
 69 | 
 70 | pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
 71 |     match input.params.name.as_str() {
 72 |         "meme_list_templates" => list_templates(input),
 73 |         "meme_get_template" => get_template(input),
 74 |         "meme_generate" => generate_meme(input),
 75 |         _ => Ok(CallToolResult {
 76 |             is_error: Some(true),
 77 |             content: vec![Content {
 78 |                 annotations: None,
 79 |                 text: Some(format!("Unknown tool: {}", input.params.name)),
 80 |                 mime_type: None,
 81 |                 r#type: ContentType::Text,
 82 |                 data: None,
 83 |             }],
 84 |         }),
 85 |     }
 86 | }
 87 | 
 88 | fn list_templates(_input: CallToolRequest) -> Result<CallToolResult, Error> {
 89 |     let templates_json = embedded::TEMPLATES_JSON;
 90 |     let templates: Vec<MemeTemplate> = serde_json::from_str(templates_json)?;
 91 | 
 92 |     Ok(CallToolResult {
 93 |         is_error: None,
 94 |         content: vec![Content {
 95 |             annotations: None,
 96 |             text: Some(serde_json::to_string_pretty(&templates)?),
 97 |             mime_type: Some("application/json".to_string()),
 98 |             r#type: ContentType::Text,
 99 |             data: None,
100 |         }],
101 |     })
102 | }
103 | 
104 | fn get_template(input: CallToolRequest) -> Result<CallToolResult, Error> {
105 |     let args = input.params.arguments.unwrap_or_default();
106 |     let template_id = args
107 |         .get("template_id")
108 |         .and_then(|v| v.as_str())
109 |         .ok_or_else(|| Error::msg("template_id is required"))?;
110 | 
111 |     let templates: Vec<MemeTemplate> = serde_json::from_str(embedded::TEMPLATES_JSON)?;
112 | 
113 |     let template = templates
114 |         .iter()
115 |         .find(|t| t.id == template_id)
116 |         .ok_or_else(|| Error::msg("Template not found"))?;
117 | 
118 |     Ok(CallToolResult {
119 |         is_error: None,
120 |         content: vec![Content {
121 |             annotations: None,
122 |             text: Some(serde_json::to_string_pretty(&template)?),
123 |             mime_type: Some("application/json".to_string()),
124 |             r#type: ContentType::Text,
125 |             data: None,
126 |         }],
127 |     })
128 | }
129 | 
130 | fn generate_meme(input: CallToolRequest) -> Result<CallToolResult, Error> {
131 |     let args = input.params.arguments.unwrap_or_default();
132 | 
133 |     let template_id = args
134 |         .get("template_id")
135 |         .and_then(|v| v.as_str())
136 |         .ok_or_else(|| Error::msg("template_id is required"))?;
137 | 
138 |     let texts = args
139 |         .get("texts")
140 |         .and_then(|v| v.as_array())
141 |         .ok_or_else(|| Error::msg("texts array is required"))?;
142 | 
143 |     // Load template configuration
144 |     let config = TemplateConfig::load(template_id)?;
145 | 
146 |     // Get the default image from embedded resources
147 |     let image_name = if embedded::get_template_image(template_id, "default.jpg").is_some() {
148 |         "default.jpg"
149 |     } else if embedded::get_template_image(template_id, "default.png").is_some() {
150 |         "default.png"
151 |     } else {
152 |         return Err(Error::msg(format!(
153 |             "No default template image found for {}",
154 |             template_id
155 |         )));
156 |     };
157 | 
158 |     let image_data = embedded::get_template_image(template_id, image_name).ok_or_else(|| {
159 |         Error::msg(format!(
160 |             "Template image {} {} not found",
161 |             template_id, image_name
162 |         ))
163 |     })?;
164 | 
165 |     let mut image = image::load_from_memory(image_data)?.to_rgba8();
166 |     let (image_width, image_height) = image.dimensions();
167 | 
168 |     let font = FontArc::try_from_slice(embedded::FONT_DATA)?;
169 | 
170 |     // Draw each text configuration
171 |     for (i, text_config) in config.text.iter().enumerate() {
172 |         if i >= texts.len() {
173 |             break;
174 |         }
175 | 
176 |         let text = texts[i]
177 |             .as_str()
178 |             .ok_or_else(|| Error::msg("Invalid text entry"))?;
179 | 
180 |         let text = if text_config.style == "upper" {
181 |             text.to_uppercase()
182 |         } else {
183 |             text.to_string()
184 |         };
185 | 
186 |         // Calculate initial desired height based on image dimensions and config scale
187 |         let desired_height = (image_height as f32 * text_config.scale_y).max(1.0);
188 | 
189 |         // Calculate maximum available width based on alignment
190 |         let padding = image_width as f32 * 0.05; // 5% padding on each side
191 |         let available_width = match text_config.align.as_str() {
192 |             "center" => image_width as f32 - (2.0 * padding),
193 |             "left" => {
194 |                 image_width as f32 - (image_width as f32 * text_config.anchor_x) - (2.0 * padding)
195 |             }
196 |             "right" => (image_width as f32 * text_config.anchor_x) - (2.0 * padding),
197 |             _ => image_width as f32 - (2.0 * padding),
198 |         };
199 | 
200 |         // Calculate appropriate scale that prevents overflow
201 |         let scale = calculate_max_scale(&font, &text, available_width, desired_height);
202 | 
203 |         // Calculate text width for positioning using the adjusted scale
204 |         let text_width = calculate_text_width(&font, &text, scale);
205 | 
206 |         // Calculate x position based on anchor and alignment, now with padding
207 |         let x = match text_config.align.as_str() {
208 |             "center" => ((image_width as f32 - text_width) / 2.0
209 |                 + (image_width as f32 * text_config.anchor_x))
210 |                 .max(padding) as i32,
211 |             "left" => ((image_width as f32 * text_config.anchor_x) + padding) as i32,
212 |             "right" => ((image_width as f32 * text_config.anchor_x) - text_width - padding)
213 |                 .max(padding) as i32,
214 |             _ => ((image_width as f32 - text_width) / 2.0).max(padding) as i32,
215 |         };
216 | 
217 |         // Calculate y position based on anchor
218 |         let y = (image_height as f32 * text_config.anchor_y) as i32;
219 | 
220 |         // Convert color string to RGBA
221 |         let color = color_to_rgba(&text_config.color);
222 | 
223 |         draw_text_mut(&mut image, color, x, y, scale, &font, &text);
224 |     }
225 | 
226 |     // Convert image to bytes
227 |     let mut output_bytes = Vec::new();
228 |     let dynamic_image = image::DynamicImage::ImageRgba8(image);
229 |     dynamic_image.write_to(&mut Cursor::new(&mut output_bytes), image::ImageFormat::Png)?;
230 | 
231 |     Ok(CallToolResult {
232 |         is_error: None,
233 |         content: vec![Content {
234 |             annotations: None,
235 |             text: None,
236 |             mime_type: Some("image/png".to_string()),
237 |             r#type: ContentType::Image,
238 |             data: Some(base64::engine::general_purpose::STANDARD.encode(&output_bytes)),
239 |         }],
240 |     })
241 | }
242 | 
243 | fn calculate_text_width(font: &FontArc, text: &str, scale: PxScale) -> f32 {
244 |     let scaled_font = font.as_scaled(scale);
245 |     let mut width = 0.0;
246 | 
247 |     for c in text.chars() {
248 |         let id = scaled_font.glyph_id(c);
249 |         width += scaled_font.h_advance(id);
250 | 
251 |         if let Some(next_char) = text.chars().nth(1) {
252 |             let next_id = scaled_font.glyph_id(next_char);
253 |             width += scaled_font.kern(id, next_id);
254 |         }
255 |     }
256 | 
257 |     width
258 | }
259 | 
260 | fn color_to_rgba(color: &str) -> Rgba<u8> {
261 |     match color.to_lowercase().as_str() {
262 |         "white" => Rgba([255, 255, 255, 255]),
263 |         "black" => Rgba([0, 0, 0, 255]),
264 |         "red" => Rgba([255, 0, 0, 255]),
265 |         "green" => Rgba([0, 255, 0, 255]),
266 |         "blue" => Rgba([0, 0, 255, 255]),
267 |         _ => Rgba([255, 255, 255, 255]), // Fallback to white
268 |     }
269 | }
270 | 
271 | fn calculate_max_scale(
272 |     font: &FontArc,
273 |     text: &str,
274 |     target_width: f32,
275 |     desired_height: f32,
276 | ) -> PxScale {
277 |     let initial_scale = PxScale::from(desired_height);
278 |     let initial_width = calculate_text_width(font, text, initial_scale);
279 | 
280 |     if initial_width <= target_width {
281 |         return initial_scale;
282 |     }
283 | 
284 |     // Scale down proportionally if text is too wide
285 |     let scale_factor = target_width / initial_width;
286 |     PxScale::from(desired_height * scale_factor)
287 | }
288 | 
289 | impl TemplateConfig {
290 |     fn load(template_id: &str) -> Result<Self, Error> {
291 |         let config_contents = embedded::get_template_config(template_id)
292 |             .ok_or_else(|| Error::msg(format!("Template {} not found", template_id)))?;
293 | 
294 |         let config: TemplateConfig = serde_yaml::from_str(config_contents)
295 |             .map_err(|e| Error::msg(format!("Failed to parse config: {}", e)))?;
296 |         Ok(config)
297 |     }
298 | }
299 | 
300 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
301 |     Ok(ListToolsResult {
302 |         tools: vec![
303 |             ToolDescription {
304 |                 name: "meme_list_templates".into(),
305 |                 description: "Lists all available meme templates".into(),
306 |                 input_schema: json!({
307 |                     "type": "object",
308 |                     "properties": {},
309 |                     "required": []
310 |                 })
311 |                 .as_object()
312 |                 .unwrap()
313 |                 .clone(),
314 |             },
315 |             ToolDescription {
316 |                 name: "meme_get_template".into(),
317 |                 description: "Get details about a specific meme template".into(),
318 |                 input_schema: json!({
319 |                     "type": "object",
320 |                     "properties": {
321 |                         "template_id": {
322 |                             "type": "string",
323 |                             "description": "The ID of the template to retrieve",
324 |                         }
325 |                     },
326 |                     "required": ["template_id"]
327 |                 })
328 |                 .as_object()
329 |                 .unwrap()
330 |                 .clone(),
331 |             },
332 |             ToolDescription {
333 |                 name: "meme_generate".into(),
334 |                 description: "Generate a meme using a template and custom text".into(),
335 |                 input_schema: json!({
336 |                     "type": "object",
337 |                     "properties": {
338 |                         "template_id": {
339 |                             "type": "string",
340 |                             "description": "The ID of the template to use",
341 |                         },
342 |                         "texts": {
343 |                             "type": "array",
344 |                             "items": {
345 |                                 "type": "string"
346 |                             },
347 |                             "description": "Array of text strings to place on the meme",
348 |                         }
349 |                     },
350 |                     "required": ["template_id", "texts"]
351 |                 })
352 |                 .as_object()
353 |                 .unwrap()
354 |                 .clone(),
355 |             },
356 |         ],
357 |     })
358 | }
359 | 
```
Page 4/11FirstPrevNextLast