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 |
```