This is page 2 of 8. Use http://codebase.md/tuananh/hyper-mcp?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/fetch/src/lib.rs:
--------------------------------------------------------------------------------
```rust
mod pdk;
use std::collections::BTreeMap;
use extism_pdk::*;
use htmd::HtmlToMarkdown;
use json::Value;
use pdk::types::{
CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
};
use serde_json::json;
pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
match input.params.name.as_str() {
"fetch" => fetch(input),
_ => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Unknown tool: {}", input.params.name)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
}
fn fetch(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.unwrap_or_default();
if let Some(Value::String(url)) = args.get("url") {
// Create HTTP request
let mut req = HttpRequest {
url: url.clone(),
headers: BTreeMap::new(),
method: Some("GET".to_string()),
};
// Add a user agent header to be polite
req.headers
.insert("User-Agent".to_string(), "fetch-tool/1.0".to_string());
// Perform the request
let res = http::request::<()>(&req, None)?;
// Convert response body to string
let body = res.body();
let html = String::from_utf8_lossy(body.as_slice());
let converter = HtmlToMarkdown::builder()
.skip_tags(vec!["script", "style"])
.build();
// Convert HTML to markdown
match converter.convert(&html) {
Ok(markdown) => Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(markdown),
mime_type: Some("text/markdown".to_string()),
r#type: ContentType::Text,
data: None,
}],
}),
Err(e) => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Failed to convert HTML to markdown: {}", e)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide a url".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
pub(crate) fn describe() -> Result<ListToolsResult, Error> {
Ok(ListToolsResult{
tools: vec![
ToolDescription {
name: "fetch".into(),
description: "Enables to open and access arbitrary text URLs. Fetches the contents of a URL and returns its contents converted to markdown".into(),
input_schema: json!({
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to fetch",
},
},
"required": ["url"],
})
.as_object()
.unwrap()
.clone(),
},
],
})
}
```
--------------------------------------------------------------------------------
/templates/plugins/go/main.go:
--------------------------------------------------------------------------------
```go
package main
import (
"fmt"
)
// Execute a tool call. This is the primary entry point for tool execution in plugins.
//
// The plugin receives a tool call request with the tool name and arguments, along with request context information. The plugin should execute the requested tool and return the result with content blocks and optional structured output.
// It takes CallToolRequest as input ()
// And returns CallToolResult ()
func CallTool(input CallToolRequest) (*CallToolResult, error) {
return nil, fmt.Errorf("CallTool not implemented.")
}
// Provide completion suggestions for a partially-typed input.
//
// This function is called when the user requests autocompletion. The plugin should analyze the partial input and return matching completion suggestions based on the reference (prompt or resource) and argument context.
// It takes CompleteRequest as input ()
// And returns CompleteResult ()
func Complete(input CompleteRequest) (*CompleteResult, error) {
return &CompleteResult{}, nil
}
// Retrieve a specific prompt by name.
//
// This function is called when the user requests a specific prompt. The plugin should return the prompt details including messages and optional description.
// It takes GetPromptRequest as input ()
// And returns GetPromptResult ()
func GetPrompt(input GetPromptRequest) (*GetPromptResult, error) {
// TODO: fill out your implementation here
return nil, fmt.Errorf("GetPrompt not implemented.")
}
// List all available prompts.
//
// This function should return a list of prompts that the plugin provides. Each prompt should include its name and a brief description of what it does. Supports pagination via cursor.
// It takes ListPromptsRequest as input ()
// And returns ListPromptsResult ()
func ListPrompts(input ListPromptsRequest) (*ListPromptsResult, error) {
// TODO: fill out your implementation here
return &ListPromptsResult{}, nil
}
// List all available resource templates.
//
// This function should return a list of resource templates that the plugin provides. Templates are URI patterns that can match multiple resources. Supports pagination via cursor.
// It takes ListResourceTemplatesRequest as input ()
// And returns ListResourceTemplatesResult ()
func ListResourceTemplates(input ListResourceTemplatesRequest) (*ListResourceTemplatesResult, error) {
// TODO: fill out your implementation here
return &ListResourceTemplatesResult{}, nil
}
// List all available resources.
//
// This function should return a list of resources that the plugin provides. Resources are URI-based references to files, data, or services. Supports pagination via cursor.
// It takes ListResourcesRequest as input ()
// And returns ListResourcesResult ()
func ListResources(input ListResourcesRequest) (*ListResourcesResult, error) {
// TODO: fill out your implementation here
return &ListResourcesResult{}, nil
}
// List all available tools.
//
// This function should return a list of all tools that the plugin provides. Each tool should include its name, description, and input schema. Supports pagination via cursor.
// It takes ListToolsRequest as input ()
// And returns ListToolsResult ()
func ListTools(input ListToolsRequest) (*ListToolsResult, error) {
// TODO: fill out your implementation here
return &ListToolsResult{}, nil
}
// Notification that the list of roots has changed.
//
// This is an optional notification handler. If implemented, the plugin will be notified whenever the roots list changes on the client side. This allows plugins to react to changes in the file system roots or other root resources.
// It takes PluginNotificationContext as input ()
func OnRootsListChanged(input PluginNotificationContext) error {
// TODO: fill out your implementation here
return nil
}
// Read the contents of a resource by its URI.
//
// This function is called when the user wants to read the contents of a specific resource. The plugin should retrieve and return the resource data with appropriate MIME type information.
// It takes ReadResourceRequest as input ()
// And returns ReadResourceResult ()
func ReadResource(input ReadResourceRequest) (*ReadResourceResult, error) {
// TODO: fill out your implementation here
return nil, fmt.Errorf("ReadResource not implemented.")
}
// Note: leave this in place, as the Go compiler will find the `export` function as the entrypoint.
func main() {}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/time/src/lib.rs:
--------------------------------------------------------------------------------
```rust
mod pdk;
use extism_pdk::*;
use pdk::types::{CallToolResult, Content, ContentType, ToolDescription};
use pdk::*;
use serde_json::json;
use std::error::Error as StdError;
use chrono::Utc;
#[derive(Debug)]
struct CustomError(String);
impl std::fmt::Display for CustomError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl StdError for CustomError {}
// Called when the tool is invoked.
pub(crate) fn call(input: types::CallToolRequest) -> Result<types::CallToolResult, Error> {
let args = input.params.arguments.unwrap_or_default();
let name = args.get("name").unwrap().as_str().unwrap();
match name {
"get_time_utc" => {
let now = Utc::now();
let timestamp = now.timestamp().to_string();
let rfc2822 = now.to_rfc2822().to_string();
Ok(CallToolResult {
content: vec![Content {
text: Some(
json!({
"utc_time": timestamp,
"utc_time_rfc2822": rfc2822,
})
.to_string(),
),
r#type: ContentType::Text,
..Default::default()
}],
is_error: Some(false),
})
}
"parse_time" => {
let time = args.get("time_rfc2822").unwrap().as_str().unwrap();
let t = chrono::DateTime::parse_from_rfc2822(time).unwrap();
let timestamp = t.timestamp().to_string();
let rfc2822 = t.to_rfc2822().to_string();
Ok(CallToolResult {
content: vec![Content {
text: Some(
json!({
"utc_time": timestamp,
"utc_time_rfc2822": rfc2822,
})
.to_string(),
),
r#type: ContentType::Text,
..Default::default()
}],
is_error: Some(false),
})
}
"time_offset" => {
let t1 = args.get("timestamp").unwrap().as_i64().unwrap();
let offset = args.get("offset").unwrap().as_i64().unwrap();
let t1 = chrono::DateTime::from_timestamp(t1, 0).unwrap();
let t2 = t1 + chrono::Duration::seconds(offset);
let timestamp = t2.timestamp().to_string();
let rfc2822 = t2.to_rfc2822().to_string();
Ok(CallToolResult {
content: vec![Content {
text: Some(
json!({
"utc_time": timestamp,
"utc_time_rfc2822": rfc2822,
})
.to_string(),
),
r#type: ContentType::Text,
..Default::default()
}],
is_error: Some(false),
})
}
_ => Err(Error::new(CustomError("unknown command".to_string()))),
}
}
pub(crate) fn describe() -> Result<types::ListToolsResult, Error> {
Ok(types::ListToolsResult { tools: vec![ToolDescription {
name: "time".into(),
description: "Time operations plugin. It provides the following operations:
- `get_time_utc`: Returns the current time in the UTC timezone. Takes no parameters.
- `parse_time`: Takes a `time_rfc2822` string in RFC2822 format and returns the timestamp in UTC timezone.
- `time_offset`: Takes integer `timestamp` and `offset` parameters. Adds a time offset to a given timestamp and returns the new timestamp in UTC timezone.
Always use this tool to compute time operations, especially when it is necessary
to compute time differences or offsets.".into(),
input_schema: json!({
"type": "object",
"required": ["name"],
"properties": {
"name": {
"type": "string",
"description": "The name of the operation to perform. ",
"enum": ["get_time_utc", "time_offset", "parse_time"],
},
"timestamp": {
"type": "integer",
"description": "The timestamp used for `time_offset`.",
},
"offset" : {
"type": "integer",
"description": "The offset to add to the time in seconds. ",
},
"time_rfc2822": {
"type": "string",
"description": "The time in RFC2822 format used in `parse_time`",
},
},
})
.as_object()
.unwrap()
.clone(),
}]})
}
```
--------------------------------------------------------------------------------
/examples/plugins/v2/rstime/src/pdk/imports.rs:
--------------------------------------------------------------------------------
```rust
#![allow(unused)]
use super::types::*;
use extism_pdk::{Error, Json, host_fn};
use std::result::Result;
/// create_elicitation Request user input through the client's elicitation interface.
///
/// Plugins can use this to ask users for input, decisions, or confirmations. This is useful for interactive plugins that need user guidance during tool execution. Returns the user's response with action and optional form data.
/// It takes input of CreateElicitationRequestParamWithTimeout ()
/// And it returns an output CreateElicitationResult ()
pub(crate) fn create_elicitation(
input: ElicitRequestParamWithTimeout,
) -> Result<ElicitResult, Error> {
let Json(res) = unsafe { raw_imports::create_elicitation(Json(input))? };
Ok(res)
}
/// create_message Request message creation through the client's sampling interface.
///
/// Plugins can use this to have the client create messages, typically with AI assistance. This is used when plugins need intelligent text generation or analysis. Returns the generated message with model information.
/// It takes input of CreateMessageRequestParam ()
/// And it returns an output CreateMessageResult ()
#[allow(unused)]
pub(crate) fn create_message(
input: CreateMessageRequestParam,
) -> Result<CreateMessageResult, Error> {
let Json(res) = unsafe { raw_imports::create_message(Json(input))? };
Ok(res)
}
/// list_roots List the client's root directories or resources.
///
/// Plugins can query this to discover what root resources (typically file system roots) are available on the client side. This helps plugins understand the scope of resources they can access.
/// And it returns an output ListRootsResult ()
pub(crate) fn list_roots() -> Result<ListRootsResult, Error> {
let Json(res) = unsafe { raw_imports::list_roots()? };
Ok(res)
}
/// notify_logging_message Send a logging message to the client.
///
/// Plugins use this to report diagnostic, informational, warning, or error messages. The client's logging level determines which messages are processed.
/// It takes input of LoggingMessageNotificationParam ()
pub(crate) fn notify_logging_message(input: LoggingMessageNotificationParam) -> Result<(), Error> {
unsafe { raw_imports::notify_logging_message(Json(input))? }
Ok(())
}
/// notify_progress Send a progress notification to the client.
///
/// Plugins use this to report progress during long-running operations. This allows clients to display progress bars or status information to users.
/// It takes input of ProgressNotificationParam ()
pub(crate) fn notify_progress(input: ProgressNotificationParam) -> Result<(), Error> {
unsafe { raw_imports::notify_progress(Json(input))? }
Ok(())
}
/// notify_prompt_list_changed Notify the client that the list of available prompts has changed.
///
/// Plugins should call this when they add, remove, or modify their available prompts. The client will typically refresh its prompt list in response.
pub(crate) fn notify_prompt_list_changed() -> Result<(), Error> {
unsafe { raw_imports::notify_prompt_list_changed()? }
Ok(())
}
/// notify_resource_list_changed Notify the client that the list of available resources has changed.
///
/// Plugins should call this when they add, remove, or modify their available resources. The client will typically refresh its resource list in response.
pub(crate) fn notify_resource_list_changed() -> Result<(), Error> {
unsafe { raw_imports::notify_resource_list_changed()? }
Ok(())
}
/// notify_resource_updated Notify the client that a specific resource has been updated.
///
/// Plugins should call this when they modify the contents of a resource. The client can use this to invalidate caches and refresh resource displays.
/// It takes input of types::ResourceUpdatedNotificationParam ()
pub(crate) fn notify_resource_updated(
input: ResourceUpdatedNotificationParam,
) -> Result<(), Error> {
unsafe { raw_imports::notify_resource_updated(Json(input))? }
Ok(())
}
/// notify_tool_list_changed Notify the client that the list of available tools has changed.
///
/// Plugins should call this when they add, remove, or modify their available tools. The client will typically refresh its tool list in response.
pub(crate) fn notify_tool_list_changed() -> Result<(), Error> {
unsafe { raw_imports::notify_tool_list_changed()? }
Ok(())
}
mod raw_imports {
use super::*;
#[host_fn]
extern "ExtismHost" {
pub(crate) fn create_elicitation(
input: Json<ElicitRequestParamWithTimeout>,
) -> Json<ElicitResult>;
pub(crate) fn create_message(
input: Json<CreateMessageRequestParam>,
) -> Json<CreateMessageResult>;
pub(crate) fn list_roots() -> Json<ListRootsResult>;
pub(crate) fn notify_logging_message(input: Json<LoggingMessageNotificationParam>);
pub(crate) fn notify_progress(input: Json<ProgressNotificationParam>);
pub(crate) fn notify_prompt_list_changed();
pub(crate) fn notify_resource_list_changed();
pub(crate) fn notify_resource_updated(input: Json<ResourceUpdatedNotificationParam>);
pub(crate) fn notify_tool_list_changed();
}
}
```
--------------------------------------------------------------------------------
/templates/plugins/rust/src/pdk/imports.rs:
--------------------------------------------------------------------------------
```rust
#![allow(unused)]
use super::types::*;
use extism_pdk::{Error, Json, host_fn};
use std::result::Result;
/// create_elicitation Request user input through the client's elicitation interface.
///
/// Plugins can use this to ask users for input, decisions, or confirmations. This is useful for interactive plugins that need user guidance during tool execution. Returns the user's response with action and optional form data.
/// It takes input of CreateElicitationRequestParamWithTimeout ()
/// And it returns an output CreateElicitationResult ()
pub(crate) fn create_elicitation(
input: ElicitRequestParamWithTimeout,
) -> Result<ElicitResult, Error> {
let Json(res) = unsafe { raw_imports::create_elicitation(Json(input))? };
Ok(res)
}
/// create_message Request message creation through the client's sampling interface.
///
/// Plugins can use this to have the client create messages, typically with AI assistance. This is used when plugins need intelligent text generation or analysis. Returns the generated message with model information.
/// It takes input of CreateMessageRequestParam ()
/// And it returns an output CreateMessageResult ()
#[allow(unused)]
pub(crate) fn create_message(
input: CreateMessageRequestParam,
) -> Result<CreateMessageResult, Error> {
let Json(res) = unsafe { raw_imports::create_message(Json(input))? };
Ok(res)
}
/// list_roots List the client's root directories or resources.
///
/// Plugins can query this to discover what root resources (typically file system roots) are available on the client side. This helps plugins understand the scope of resources they can access.
/// And it returns an output ListRootsResult ()
pub(crate) fn list_roots() -> Result<ListRootsResult, Error> {
let Json(res) = unsafe { raw_imports::list_roots()? };
Ok(res)
}
/// notify_logging_message Send a logging message to the client.
///
/// Plugins use this to report diagnostic, informational, warning, or error messages. The client's logging level determines which messages are processed.
/// It takes input of LoggingMessageNotificationParam ()
pub(crate) fn notify_logging_message(input: LoggingMessageNotificationParam) -> Result<(), Error> {
unsafe { raw_imports::notify_logging_message(Json(input))? }
Ok(())
}
/// notify_progress Send a progress notification to the client.
///
/// Plugins use this to report progress during long-running operations. This allows clients to display progress bars or status information to users.
/// It takes input of ProgressNotificationParam ()
pub(crate) fn notify_progress(input: ProgressNotificationParam) -> Result<(), Error> {
unsafe { raw_imports::notify_progress(Json(input))? }
Ok(())
}
/// notify_prompt_list_changed Notify the client that the list of available prompts has changed.
///
/// Plugins should call this when they add, remove, or modify their available prompts. The client will typically refresh its prompt list in response.
pub(crate) fn notify_prompt_list_changed() -> Result<(), Error> {
unsafe { raw_imports::notify_prompt_list_changed()? }
Ok(())
}
/// notify_resource_list_changed Notify the client that the list of available resources has changed.
///
/// Plugins should call this when they add, remove, or modify their available resources. The client will typically refresh its resource list in response.
pub(crate) fn notify_resource_list_changed() -> Result<(), Error> {
unsafe { raw_imports::notify_resource_list_changed()? }
Ok(())
}
/// notify_resource_updated Notify the client that a specific resource has been updated.
///
/// Plugins should call this when they modify the contents of a resource. The client can use this to invalidate caches and refresh resource displays.
/// It takes input of types::ResourceUpdatedNotificationParam ()
pub(crate) fn notify_resource_updated(
input: ResourceUpdatedNotificationParam,
) -> Result<(), Error> {
unsafe { raw_imports::notify_resource_updated(Json(input))? }
Ok(())
}
/// notify_tool_list_changed Notify the client that the list of available tools has changed.
///
/// Plugins should call this when they add, remove, or modify their available tools. The client will typically refresh its tool list in response.
pub(crate) fn notify_tool_list_changed() -> Result<(), Error> {
unsafe { raw_imports::notify_tool_list_changed()? }
Ok(())
}
mod raw_imports {
use super::*;
#[host_fn]
extern "ExtismHost" {
pub(crate) fn create_elicitation(
input: Json<ElicitRequestParamWithTimeout>,
) -> Json<ElicitResult>;
pub(crate) fn create_message(
input: Json<CreateMessageRequestParam>,
) -> Json<CreateMessageResult>;
pub(crate) fn list_roots() -> Json<ListRootsResult>;
pub(crate) fn notify_logging_message(input: Json<LoggingMessageNotificationParam>);
pub(crate) fn notify_progress(input: Json<ProgressNotificationParam>);
pub(crate) fn notify_prompt_list_changed();
pub(crate) fn notify_resource_list_changed();
pub(crate) fn notify_resource_updated(input: Json<ResourceUpdatedNotificationParam>);
pub(crate) fn notify_tool_list_changed();
}
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/eval-py/src/lib.rs:
--------------------------------------------------------------------------------
```rust
mod pdk;
use rustpython_vm::{self as vm, Settings, scope::Scope};
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use extism_pdk::*;
use json::Value;
use pdk::types::{
CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
};
use serde_json::json;
struct StoredVirtualMachine {
interp: vm::Interpreter,
scope: Scope,
}
impl StoredVirtualMachine {
fn new() -> Self {
let mut scope = None;
let mut settings = Settings::default();
settings.allow_external_library = false;
let interp = vm::Interpreter::with_init(settings, |vm| {
scope = Some(vm.new_scope_with_builtins());
});
StoredVirtualMachine {
interp,
scope: scope.unwrap(),
}
}
}
thread_local! {
static STORED_VMS: RefCell<HashMap<String, Rc<StoredVirtualMachine>>> = RefCell::default();
}
fn get_or_create_vm(id: &str) -> Rc<StoredVirtualMachine> {
STORED_VMS.with(|cell| {
let mut vms = cell.borrow_mut();
if !vms.contains_key(id) {
let stored_vm = StoredVirtualMachine::new();
vms.insert(id.to_string(), Rc::new(stored_vm));
}
vms.get(id).unwrap().clone()
})
}
pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
match input.params.name.as_str() {
"eval_python" => eval_python(input),
_ => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Unknown tool: {}", input.params.name)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
}
fn eval_python(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.unwrap_or_default();
if let Some(Value::String(code)) = args.get("code") {
let stored_vm = get_or_create_vm("eval_python");
let result = stored_vm.interp.enter(|vm| {
match vm
.compile(code, vm::compiler::Mode::Single, "<eval>".to_owned())
.map_err(|err| vm.new_syntax_error(&err, Some(code)))
.and_then(|code_obj| vm.run_code_obj(code_obj, stored_vm.scope.clone()))
{
Ok(output) => {
if !vm.is_none(&output) {
stored_vm
.scope
.globals
.set_item("last", output.clone(), vm)?;
match output.str(vm) {
Ok(s) => Ok(s.to_string()),
Err(e) => Err(e),
}
} else {
Ok("None".to_string())
}
}
Err(exc) => Err(exc),
}
});
match result {
Ok(output) => Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(output),
mime_type: Some("text/plain".to_string()),
r#type: ContentType::Text,
data: None,
}],
}),
Err(exc) => {
let mut error_msg = String::new();
stored_vm.interp.enter(|vm| {
vm.write_exception(&mut error_msg, &exc).unwrap_or_default();
});
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(error_msg),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide Python code to evaluate".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
pub(crate) fn describe() -> Result<ListToolsResult, Error> {
Ok(ListToolsResult{
tools: vec![
ToolDescription {
name: "eval_python".into(),
description: "Evaluates Python code using RustPython and returns the result. Use this like how you would use a REPL. This won't return the output of the code, but the result of the last expression.".into(),
input_schema: json!({
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "The Python code to evaluate",
},
},
"required": ["code"],
})
.as_object()
.unwrap()
.clone(),
},
],
})
}
```
--------------------------------------------------------------------------------
/templates/plugins/go/imports.go:
--------------------------------------------------------------------------------
```go
package main
import pdk "github.com/extism/go-pdk"
// CreateElicitation Request user input through the client's elicitation interface.
//
// Plugins can use this to ask users for input, decisions, or confirmations. This is useful for interactive plugins that need user guidance during tool execution. Returns the user's response with action and optional form data.
// It takes input of CreateElicitationRequestParamWithTimeout ()
// And it returns an output *CreateElicitationResult ()
func CreateElicitation(input ElicitRequestParamWithTimeout) (*ElicitResult, error) {
var err error
_ = err
mem, err := pdk.AllocateJSON(&input)
if err != nil {
return nil, err
}
offs := _CreateElicitation(mem.Offset())
var out ElicitResult
err = pdk.JSONFrom(offs, &out)
if err != nil {
return nil, err
}
return &out, nil
}
// CreateMessage Request message creation through the client's sampling interface.
//
// Plugins can use this to have the client create messages, typically with AI assistance. This is used when plugins need intelligent text generation or analysis. Returns the generated message with model information.
// It takes input of CreateMessageRequestParam ()
// And it returns an output *CreateMessageResult ()
func CreateMessage(input CreateMessageRequestParam) (*CreateMessageResult, error) {
var err error
_ = err
mem, err := pdk.AllocateJSON(&input)
if err != nil {
return nil, err
}
offs := _CreateMessage(mem.Offset())
var out CreateMessageResult
err = pdk.JSONFrom(offs, &out)
if err != nil {
return nil, err
}
return &out, nil
}
// ListRoots List the client's root directories or resources.
//
// Plugins can query this to discover what root resources (typically file system roots) are available on the client side. This helps plugins understand the scope of resources they can access.
// And it returns an output *ListRootsResult ()
func ListRoots() (*ListRootsResult, error) {
var err error
_ = err
offs := _ListRoots()
var out ListRootsResult
err = pdk.JSONFrom(offs, &out)
if err != nil {
return nil, err
}
return &out, nil
}
// NotifyLoggingMessage Send a logging message to the client.
//
// Plugins use this to report diagnostic, informational, warning, or error messages. The client's logging level determines which messages are processed.
// It takes input of LoggingMessageNotificationParam ()
func NotifyLoggingMessage(input LoggingMessageNotificationParam) error {
var err error
_ = err
mem, err := pdk.AllocateJSON(&input)
if err != nil {
return err
}
_NotifyLoggingMessage(mem.Offset())
return nil
}
// NotifyProgress Send a progress notification to the client.
//
// Plugins use this to report progress during long-running operations. This allows clients to display progress bars or status information to users.
// It takes input of ProgressNotificationParam ()
func NotifyProgress(input ProgressNotificationParam) error {
var err error
_ = err
mem, err := pdk.AllocateJSON(&input)
if err != nil {
return err
}
_NotifyProgress(mem.Offset())
return nil
}
// NotifyPromptListChanged Notify the client that the list of available prompts has changed.
//
// Plugins should call this when they add, remove, or modify their available prompts. The client will typically refresh its prompt list in response.
func NotifyPromptListChanged() error {
var err error
_ = err
_NotifyPromptListChanged()
return nil
}
// NotifyResourceListChanged Notify the client that the list of available resources has changed.
//
// Plugins should call this when they add, remove, or modify their available resources. The client will typically refresh its resource list in response.
func NotifyResourceListChanged() error {
var err error
_ = err
_NotifyResourceListChanged()
return nil
}
// NotifyResourceUpdated Notify the client that a specific resource has been updated.
//
// Plugins should call this when they modify the contents of a resource. The client can use this to invalidate caches and refresh resource displays.
// It takes input of ResourceUpdatedNotificationParam ()
func NotifyResourceUpdated(input ResourceUpdatedNotificationParam) error {
var err error
_ = err
mem, err := pdk.AllocateJSON(&input)
if err != nil {
return err
}
_NotifyResourceUpdated(mem.Offset())
return nil
}
// NotifyToolListChanged Notify the client that the list of available tools has changed.
//
// Plugins should call this when they add, remove, or modify their available tools. The client will typically refresh its tool list in response.
func NotifyToolListChanged() error {
var err error
_ = err
_NotifyToolListChanged()
return nil
}
//go:wasmimport extism:host/user create_elicitation
func _CreateElicitation(uint64) uint64
//go:wasmimport extism:host/user create_message
func _CreateMessage(uint64) uint64
//go:wasmimport extism:host/user list_roots
func _ListRoots() uint64
//go:wasmimport extism:host/user notify_logging_message
func _NotifyLoggingMessage(uint64)
//go:wasmimport extism:host/user notify_progress
func _NotifyProgress(uint64)
//go:wasmimport extism:host/user notify_prompt_list_changed
func _NotifyPromptListChanged()
//go:wasmimport extism:host/user notify_resource_list_changed
func _NotifyResourceListChanged()
//go:wasmimport extism:host/user notify_resource_updated
func _NotifyResourceUpdated(uint64)
//go:wasmimport extism:host/user notify_tool_list_changed
func _NotifyToolListChanged()
```
--------------------------------------------------------------------------------
/examples/plugins/v1/tool-list-changed/src/lib.rs:
--------------------------------------------------------------------------------
```rust
mod pdk;
use extism_pdk::*;
use pdk::types::{CallToolResult, Content, ContentType, ListToolsResult, ToolDescription};
use pdk::*;
use serde_json::json;
use std::error::Error as StdError;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug)]
struct CustomError(String);
impl std::fmt::Display for CustomError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl StdError for CustomError {}
// Global counter to track how many tools have been added
static TOOL_COUNT: AtomicUsize = AtomicUsize::new(0);
#[host_fn("extism:host/user")]
extern "ExtismHost" {
fn notify_tool_list_changed();
}
// Called when a tool is invoked
pub(crate) fn call(input: types::CallToolRequest) -> Result<types::CallToolResult, Error> {
let tool_name = &input.params.name;
match tool_name.as_str() {
"add_tool" => {
// Increment the tool count
let new_count = TOOL_COUNT.fetch_add(1, Ordering::SeqCst) + 1;
// Notify that the tool list has changed
match unsafe { notify_tool_list_changed() } {
Ok(()) => Ok(CallToolResult {
content: vec![Content {
text: Some(
json!({
"message": format!("Successfully added tool_{}", new_count),
"tool_count": new_count,
})
.to_string(),
),
r#type: ContentType::Text,
..Default::default()
}],
is_error: Some(false),
}),
Err(e) => Ok(CallToolResult {
content: vec![Content {
text: Some(format!("Failed to notify host of tool list change: {}", e)),
r#type: ContentType::Text,
..Default::default()
}],
is_error: Some(true),
}),
}
}
tool_name if tool_name.starts_with("tool_") => {
// Handle dynamically created tools
let tool_number = tool_name.strip_prefix("tool_").unwrap_or("unknown");
// Validate that the tool exists by comparing to TOOL_COUNT
if let Ok(number_str) = std::str::from_utf8(tool_number.as_bytes()) {
if let Ok(tool_num) = number_str.parse::<usize>() {
let current_count = TOOL_COUNT.load(Ordering::SeqCst);
if tool_num < 1 || tool_num > current_count {
return Ok(CallToolResult {
content: vec![Content {
text: Some(format!(
"Tool {} does not exist. Only tools 1 through {} have been created.",
tool_num, current_count
)),
r#type: ContentType::Text,
..Default::default()
}],
is_error: Some(true),
});
}
} else {
return Ok(CallToolResult {
content: vec![Content {
text: Some(format!("Invalid tool number: {}", tool_number)),
r#type: ContentType::Text,
..Default::default()
}],
is_error: Some(true),
});
}
}
Ok(CallToolResult {
content: vec![Content {
text: Some(
json!({
"message": format!("Called dynamically created tool: {}", tool_name),
"tool_number": tool_number,
})
.to_string(),
),
r#type: ContentType::Text,
..Default::default()
}],
is_error: Some(false),
})
}
_ => Err(Error::new(CustomError(format!(
"Unknown tool: {}",
tool_name
)))),
}
}
pub(crate) fn describe() -> Result<types::ListToolsResult, Error> {
let current_count = TOOL_COUNT.load(Ordering::SeqCst);
let mut tools = vec![];
// Always include the add_tool
tools.push(ToolDescription {
name: "add_tool".into(),
description: "Adds a new dynamic tool to the plugin's tool list. Each call creates a new tool named 'tool_n' where n is the number of times this tool has been called.".into(),
input_schema: json!({
"type": "object",
"properties": {},
"additionalProperties": false
})
.as_object()
.unwrap()
.clone(),
});
// Add all the dynamically created tools
for i in 1..=current_count {
tools.push(ToolDescription {
name: format!("tool_{}", i),
description: format!(
"Dynamically created tool number {}. This tool was added by calling 'add_tool'.",
i
),
input_schema: json!({
"type": "object",
"properties": {},
"additionalProperties": false
})
.as_object()
.unwrap()
.clone(),
});
}
Ok(ListToolsResult { tools })
}
```
--------------------------------------------------------------------------------
/DEPLOYMENT.md:
--------------------------------------------------------------------------------
```markdown
Deployment
==========
## Docker
Assume you have Docker installed.
Pull the image
```sh
docker pull ghcr.io/tuananh/hyper-mcp:latest
```
Create a sample config file like this, assume at `/home/ubuntu/config.json`
```json
{
"plugins": {
"time": {
"url": "oci://ghcr.io/tuananh/time-plugin:latest"
},
"qr_code": {
"url": "oci://ghcr.io/tuananh/qrcode-plugin:latest"
}
}
}
```
> 📖 **For authentication configuration and advanced options, see [RUNTIME_CONFIG.md](./RUNTIME_CONFIG.md)**
### Authentication in Docker
For production deployments with authentication, you have several options:
**Option 1: Mount keyring (Linux only)**
```sh
docker run -d \
--name hyper-mcp \
-p 3001:3001 \
-v /home/ubuntu/config.json:/app/config.json \
-v ~/.local/share/keyrings:/home/appuser/.local/share/keyrings:ro \
ghcr.io/tuananh/hyper-mcp \
--transport sse \
--bind-address 0.0.0.0:3001 \
--config-file /app/config.json
```
**Option 2: Use Docker secrets**
```sh
# Create secrets
echo '{"type":"basic","username":"user","password":"pass"}' | docker secret create registry_auth -
# Run with secrets
docker run -d \
--name hyper-mcp \
-p 3001:3001 \
-v /home/ubuntu/config.json:/app/config.json \
--secret registry_auth \
ghcr.io/tuananh/hyper-mcp \
--transport sse \
--bind-address 0.0.0.0:3001 \
--config-file /app/config.json
```
**Option 3: Environment-based credentials**
```sh
docker run -d \
--name hyper-mcp \
-p 3001:3001 \
-v /home/ubuntu/config.json:/app/config.json \
-e REGISTRY_USER="username" \
-e REGISTRY_PASS="password" \
ghcr.io/tuananh/hyper-mcp \
--transport sse \
--bind-address 0.0.0.0:3001 \
--config-file /app/config.json
```
Run the container
```sh
docker run -d \
--name hyper-mcp \
-p 3001:3001 \
-v /home/ubuntu/config.json:/app/config.json \
ghcr.io/tuananh/hyper-mcp \
--transport sse \
--bind-address 0.0.0.0:3001 \
--config-file /app/config.json
```
Note that we need to bind to `--bind-address 0.0.0.0:3001` in order to access from the host.
## GCP Cloud Run
### Prerequisites
- Google Cloud SDK installed
- Terraform installed
- A GCP project with Cloud Run and Secret Manager APIs enabled
### Configuration
1. Create a `terraform.tfvars` file with your configuration in `iac` folder:
```hcl
name = "hyper-mcp"
project_id = "your-project-id"
region = "asia-southeast1" # or your preferred region
```
2. Create a config file in Secret Manager:
The config file will be automatically created and managed by Terraform. Here's an example of what it contains:
```json
{
"plugins": {
"time": {
"url": "oci://ghcr.io/tuananh/time-plugin:latest"
},
"qr_code": {
"url": "oci://ghcr.io/tuananh/qrcode-plugin:latest"
}
}
}
```
For production deployments with authentication, update the config to use Secret Manager:
```json
{
"auths": {
"https://private.registry.example.com": {
"type": "basic",
"username": "registry-user",
"password": "registry-password"
}
},
"plugins": {
"time": {
"url": "oci://ghcr.io/tuananh/time-plugin:latest"
},
"private_plugin": {
"url": "https://private.registry.example.com/secure-plugin:latest",
"runtime_config": {
"allowed_hosts": ["private.registry.example.com"]
}
}
}
}
```
3. Deploy using Terraform:
```sh
cd iac
terraform init
terraform plan
terraform apply
```
The service will be deployed with:
- Port 3001 exposed
- Config file mounted at `/app/config.json`
- Public access enabled
- SSE transport mode
- Bound to 0.0.0.0:3001
### Accessing the Service
After deployment, you can get the service URL using:
```sh
terraform output url
```
The service will be accessible at the provided URL.
### Authentication with GCP Secret Manager
For secure credential management in GCP Cloud Run:
1. Store authentication credentials in Secret Manager:
```sh
# Store registry credentials
gcloud secrets create registry-auth --data-file=- <<< '{"type":"basic","username":"user","password":"pass"}'
# Store API tokens
gcloud secrets create api-token --data-file=- <<< '{"type":"token","token":"your-api-token"}'
```
2. Update your Terraform configuration to mount secrets:
```hcl
resource "google_cloud_run_service" "hyper_mcp" {
# ... existing configuration ...
template {
spec {
containers {
# ... existing container config ...
env {
name = "CONFIG_FILE"
value = "/app/config.json"
}
volume_mounts {
name = "secrets"
mount_path = "/app/secrets"
}
}
volumes {
name = "secrets"
secret {
secret_name = google_secret_manager_secret.registry_auth.secret_id
}
}
}
}
}
```
## Production Security Considerations
### Authentication Best Practices
- **Never include credentials in Docker images or version control**
- **Use keyring authentication for local development**
- **Use cloud-native secret management for production** (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault)
- **Rotate credentials regularly and update keyring/secret stores**
- **Use least-privilege access principles** for service accounts
- **Monitor authentication failures** in logs
### Container Security
- **Run containers with non-root users**
- **Use read-only filesystems where possible**
- **Limit container network access**
- **Scan images for vulnerabilities regularly**
- **Use distroless or minimal base images**
## Cloudflare Workers
Not possible yet but it's in my TODO list.
```
--------------------------------------------------------------------------------
/examples/plugins/v1/crypto-price/pdk.gen.go:
--------------------------------------------------------------------------------
```go
package main
import (
"errors"
pdk "github.com/extism/go-pdk"
)
//export call
func _Call() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "Call: getting JSON input")
var input CallToolRequest
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "Call: calling implementation function")
output, err := Call(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "Call: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "Call: returning")
return 0
}
//export describe
func _Describe() int32 {
var err error
_ = err
output, err := Describe()
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "Describe: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "Describe: returning")
return 0
}
type BlobResourceContents struct {
// A base64-encoded string representing the binary data of the item.
Blob string `json:"blob"`
// The MIME type of this resource, if known.
MimeType *string `json:"mimeType,omitempty"`
// The URI of this resource.
Uri string `json:"uri"`
}
// Used by the client to invoke a tool provided by the server.
type CallToolRequest struct {
Method *string `json:"method,omitempty"`
Params Params `json:"params"`
}
// The server's response to a tool call.
//
// Any errors that originate from the tool SHOULD be reported inside the result
// object, with `isError` set to true, _not_ as an MCP protocol-level error
// response. Otherwise, the LLM would not be able to see that an error occurred
// and self-correct.
//
// However, any errors in _finding_ the tool, an error indicating that the
// server does not support tool calls, or any other exceptional conditions,
// should be reported as an MCP error response.
type CallToolResult struct {
Content []Content `json:"content"`
// Whether the tool call ended in an error.
//
// If not set, this is assumed to be false (the call was successful).
IsError *bool `json:"isError,omitempty"`
}
// A content response.
// For text content set type to ContentType.Text and set the `text` property
// For image content set type to ContentType.Image and set the `data` and `mimeType` properties
type Content struct {
Annotations *TextAnnotation `json:"annotations,omitempty"`
// The base64-encoded image data.
Data *string `json:"data,omitempty"`
// The MIME type of the image. Different providers may support different image types.
MimeType *string `json:"mimeType,omitempty"`
// The text content of the message.
Text *string `json:"text,omitempty"`
Type ContentType `json:"type"`
}
type ContentType string
const (
ContentTypeText ContentType = "text"
ContentTypeImage ContentType = "image"
ContentTypeResource ContentType = "resource"
)
func (v ContentType) String() string {
switch v {
case ContentTypeText:
return `text`
case ContentTypeImage:
return `image`
case ContentTypeResource:
return `resource`
default:
return ""
}
}
func stringToContentType(s string) (ContentType, error) {
switch s {
case `text`:
return ContentTypeText, nil
case `image`:
return ContentTypeImage, nil
case `resource`:
return ContentTypeResource, nil
default:
return ContentType(""), errors.New("unable to convert string to ContentType")
}
}
// Provides one or more descriptions of the tools available in this servlet.
type ListToolsResult struct {
// The list of ToolDescription objects provided by this servlet.
Tools []ToolDescription `json:"tools"`
}
type Params struct {
Arguments interface{} `json:"arguments,omitempty"`
Name string `json:"name"`
}
// The sender or recipient of messages and data in a conversation.
type Role string
const (
RoleAssistant Role = "assistant"
RoleUser Role = "user"
)
func (v Role) String() string {
switch v {
case RoleAssistant:
return `assistant`
case RoleUser:
return `user`
default:
return ""
}
}
func stringToRole(s string) (Role, error) {
switch s {
case `assistant`:
return RoleAssistant, nil
case `user`:
return RoleUser, nil
default:
return Role(""), errors.New("unable to convert string to Role")
}
}
// A text annotation
type TextAnnotation struct {
// Describes who the intended customer of this object or data is.
//
// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
Audience []Role `json:"audience,omitempty"`
// Describes how important this data is for operating the server.
//
// A value of 1 means "most important," and indicates that the data is
// effectively required, while 0 means "least important," and indicates that
// the data is entirely optional.
Priority float32 `json:"priority,omitempty"`
}
type TextResourceContents struct {
// The MIME type of this resource, if known.
MimeType *string `json:"mimeType,omitempty"`
// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
Text string `json:"text"`
// The URI of this resource.
Uri string `json:"uri"`
}
// Describes the capabilities and expected paramters of the tool function
type ToolDescription struct {
// A description of the tool
Description string `json:"description"`
// The JSON schema describing the argument input
InputSchema interface{} `json:"inputSchema"`
// The name of the tool. It should match the plugin / binding name.
Name string `json:"name"`
}
// Note: leave this in place, as the Go compiler will find the `export` function as the entrypoint.
func main() {}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/github/pdk.gen.go:
--------------------------------------------------------------------------------
```go
// THIS FILE WAS GENERATED BY `xtp-go-bindgen`. DO NOT EDIT.
package main
import (
"errors"
pdk "github.com/extism/go-pdk"
)
//export call
func _Call() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "Call: getting JSON input")
var input CallToolRequest
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "Call: calling implementation function")
output, err := Call(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "Call: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "Call: returning")
return 0
}
//export describe
func _Describe() int32 {
var err error
_ = err
output, err := Describe()
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "Describe: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "Describe: returning")
return 0
}
//
type BlobResourceContents struct {
// A base64-encoded string representing the binary data of the item.
Blob string `json:"blob"`
// The MIME type of this resource, if known.
MimeType *string `json:"mimeType,omitempty"`
// The URI of this resource.
Uri string `json:"uri"`
}
// Used by the client to invoke a tool provided by the server.
type CallToolRequest struct {
Method *string `json:"method,omitempty"`
Params Params `json:"params"`
}
// The server's response to a tool call.
//
// Any errors that originate from the tool SHOULD be reported inside the result
// object, with `isError` set to true, _not_ as an MCP protocol-level error
// response. Otherwise, the LLM would not be able to see that an error occurred
// and self-correct.
//
// However, any errors in _finding_ the tool, an error indicating that the
// server does not support tool calls, or any other exceptional conditions,
// should be reported as an MCP error response.
type CallToolResult struct {
Content []Content `json:"content"`
// Whether the tool call ended in an error.
//
// If not set, this is assumed to be false (the call was successful).
IsError *bool `json:"isError,omitempty"`
}
// A content response.
// For text content set type to ContentType.Text and set the `text` property
// For image content set type to ContentType.Image and set the `data` and `mimeType` properties
type Content struct {
Annotations *TextAnnotation `json:"annotations,omitempty"`
// The base64-encoded image data.
Data *string `json:"data,omitempty"`
// The MIME type of the image. Different providers may support different image types.
MimeType *string `json:"mimeType,omitempty"`
// The text content of the message.
Text *string `json:"text,omitempty"`
Type ContentType `json:"type"`
}
//
type ContentType string
const (
ContentTypeText ContentType = "text"
ContentTypeImage ContentType = "image"
ContentTypeResource ContentType = "resource"
)
func (v ContentType) String() string {
switch v {
case ContentTypeText:
return `text`
case ContentTypeImage:
return `image`
case ContentTypeResource:
return `resource`
default:
return ""
}
}
func stringToContentType(s string) (ContentType, error) {
switch s {
case `text`:
return ContentTypeText, nil
case `image`:
return ContentTypeImage, nil
case `resource`:
return ContentTypeResource, nil
default:
return ContentType(""), errors.New("unable to convert string to ContentType")
}
}
// Provides one or more descriptions of the tools available in this servlet.
type ListToolsResult struct {
// The list of ToolDescription objects provided by this servlet.
Tools []ToolDescription `json:"tools"`
}
//
type Params struct {
Arguments interface{} `json:"arguments,omitempty"`
Name string `json:"name"`
}
// The sender or recipient of messages and data in a conversation.
type Role string
const (
RoleAssistant Role = "assistant"
RoleUser Role = "user"
)
func (v Role) String() string {
switch v {
case RoleAssistant:
return `assistant`
case RoleUser:
return `user`
default:
return ""
}
}
func stringToRole(s string) (Role, error) {
switch s {
case `assistant`:
return RoleAssistant, nil
case `user`:
return RoleUser, nil
default:
return Role(""), errors.New("unable to convert string to Role")
}
}
// A text annotation
type TextAnnotation struct {
// Describes who the intended customer of this object or data is.
//
// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
Audience []Role `json:"audience,omitempty"`
// Describes how important this data is for operating the server.
//
// A value of 1 means "most important," and indicates that the data is
// effectively required, while 0 means "least important," and indicates that
// the data is entirely optional.
Priority float32 `json:"priority,omitempty"`
}
//
type TextResourceContents struct {
// The MIME type of this resource, if known.
MimeType *string `json:"mimeType,omitempty"`
// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
Text string `json:"text"`
// The URI of this resource.
Uri string `json:"uri"`
}
// Describes the capabilities and expected paramters of the tool function
type ToolDescription struct {
// A description of the tool
Description string `json:"description"`
// The JSON schema describing the argument input
InputSchema interface{} `json:"inputSchema"`
// The name of the tool. It should match the plugin / binding name.
Name string `json:"name"`
}
// Note: leave this in place, as the Go compiler will find the `export` function as the entrypoint.
func main() {}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/memory/src/lib.rs:
--------------------------------------------------------------------------------
```rust
mod pdk;
use extism_pdk::*;
use pdk::types::{
CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
};
use rusqlite::{Connection, params};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Once;
use uuid::Uuid;
static DB_INIT: Once = Once::new();
#[derive(Debug, Serialize, Deserialize)]
struct Memory {
id: String,
content: String,
}
fn init_db(db_path: &str) -> Result<(), Error> {
let conn = Connection::open_with_flags(
db_path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
)",
[],
)?;
Ok(())
}
fn get_db_path() -> Result<String, Error> {
config::get("db_path")?
.ok_or_else(|| Error::msg("db_path configuration is required but not set"))
}
fn store_memory(content: &str, db_path: &str) -> Result<String, Error> {
let conn = Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE)?;
let id = Uuid::new_v4().to_string();
conn.execute(
"INSERT INTO memories (id, content) VALUES (?, ?)",
params![id, content],
)?;
Ok(id)
}
fn get_memory(id: &str, db_path: &str) -> Result<Option<Memory>, Error> {
let conn = Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE)?;
let mut stmt = conn.prepare("SELECT id, content FROM memories WHERE id = ?")?;
let mut rows = stmt.query(params![id])?;
if let Some(row) = rows.next()? {
Ok(Some(Memory {
id: row.get(0)?,
content: row.get(1)?,
}))
} else {
Ok(None)
}
}
pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
let db_path = get_db_path()?;
DB_INIT.call_once(|| {
init_db(&db_path).expect("Failed to initialize database");
});
match input.params.name.as_str() {
"store_memory" => {
let args = input.params.arguments.unwrap_or_default();
let content = match args.get("content") {
Some(v) if v.is_string() => v.as_str().unwrap(),
_ => return Err(Error::msg("content parameter is required")),
};
let id = store_memory(content, &db_path)?;
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(json!({ "id": id }).to_string()),
mime_type: Some("application/json".to_string()),
r#type: ContentType::Text,
data: None,
}],
})
}
"get_memory" => {
let args = input.params.arguments.unwrap_or_default();
let id = match args.get("id") {
Some(v) if v.is_string() => v.as_str().unwrap(),
_ => return Err(Error::msg("id parameter is required")),
};
match get_memory(id, &db_path)? {
Some(memory) => Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(serde_json::to_string(&memory)?),
mime_type: Some("application/json".to_string()),
r#type: ContentType::Text,
data: None,
}],
}),
None => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Memory not found".to_string()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
}
_ => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Unknown tool: {}", input.params.name)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
}
pub(crate) fn describe() -> Result<ListToolsResult, Error> {
Ok(ListToolsResult {
tools: vec![
ToolDescription {
name: "store_memory".into(),
description: "Store content in memory and return a unique ID".into(),
input_schema: json!({
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The content to store",
}
},
"required": ["content"],
})
.as_object()
.unwrap()
.clone(),
},
ToolDescription {
name: "get_memory".into(),
description: "Retrieve content from memory by ID".into(),
input_schema: json!({
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The ID of the content to retrieve",
}
},
"required": ["id"],
})
.as_object()
.unwrap()
.clone(),
},
],
})
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/github/gists.go:
--------------------------------------------------------------------------------
```go
package main
import (
"encoding/json"
"fmt"
"github.com/extism/go-pdk"
)
var (
CreateGistTool = ToolDescription{
Name: "gh-create-gist",
Description: "Create a GitHub Gist",
InputSchema: schema{
"type": "object",
"properties": props{
"description": prop("string", "Description of the gist"),
"files": SchemaProperty{
Type: "object",
Description: "Files contained in the gist.",
AdditionalProperties: &schema{
"type": "object",
"properties": schema{
"content": schema{
"type": "string",
"description": "Content of the file",
},
},
"required": []string{"content"},
},
},
},
"required": []string{"files"},
},
}
GetGistTool = ToolDescription{
Name: "gh-get-gist",
Description: "Gets a specified gist.",
InputSchema: schema{
"type": "object",
"properties": props{
"gist_id": prop("string", "The unique identifier of the gist."),
},
"required": []string{"gist_id"},
},
}
UpdateGistTool = ToolDescription{
Name: "gh-update-gist",
Description: "Lists pull requests in a specified repository. Supports different response formats via accept parameter.",
InputSchema: schema{
"type": "object",
"properties": props{
"gist_id": prop("string", "The unique identifier of the gist."),
"description": prop("string", "Description of the gist"),
"files": SchemaProperty{
Type: "object",
Description: "Files contained in the gist.",
AdditionalProperties: &schema{
"type": "object",
"properties": schema{
"content": schema{
"type": "string",
"description": "Content of the file",
},
},
"required": []string{"content"},
},
},
},
"required": []string{"gist_id"},
},
}
DeleteGistTool = ToolDescription{
Name: "gh-delete-gist",
Description: "Delete a specified gist.",
InputSchema: schema{
"type": "object",
"properties": props{
"gist_id": prop("string", "The unique identifier of the gist."),
},
"required": []string{"gist_id"},
},
}
)
var GistTools = []ToolDescription{
CreateGistTool,
GetGistTool,
UpdateGistTool,
DeleteGistTool,
}
func gistCreate(apiKey, description string, files map[string]any) CallToolResult {
url := "https://api.github.com/gists"
req := pdk.NewHTTPRequest(pdk.MethodPost, url)
req.SetHeader("Authorization", fmt.Sprintf("token %s", apiKey))
req.SetHeader("Content-Type", "application/json")
req.SetHeader("Accept", "application/vnd.github+json")
req.SetHeader("User-Agent", "github-mcpx-servlet")
data := map[string]any{
"description": description,
"files": files,
}
res, err := json.Marshal(data)
if err != nil {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(fmt.Sprintf("Failed to marshal gist data: %s", err)),
}},
}
}
req.SetBody(res)
resp := req.Send()
if resp.Status() != 201 {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(fmt.Sprintf("Failed to create gist: %d %s", resp.Status(), string(resp.Body()))),
}},
}
}
return CallToolResult{
Content: []Content{{
Type: ContentTypeText,
Text: some(string(resp.Body())),
}},
}
}
func gistUpdate(apiKey, gistId, description string, files map[string]any) CallToolResult {
url := fmt.Sprintf("https://api.github.com/gists/%s", gistId)
req := pdk.NewHTTPRequest(pdk.MethodPatch, url)
req.SetHeader("Authorization", fmt.Sprintf("token %s", apiKey))
req.SetHeader("Content-Type", "application/json")
req.SetHeader("Accept", "application/vnd.github+json")
req.SetHeader("User-Agent", "github-mcpx-servlet")
data := map[string]any{
"description": description,
"files": files,
}
res, err := json.Marshal(data)
if err != nil {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(fmt.Sprintf("Failed to marshal gist data: %s", err)),
}},
}
}
req.SetBody(res)
resp := req.Send()
if resp.Status() != 201 {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(fmt.Sprintf("Failed to create gist: %d %s", resp.Status(), string(resp.Body()))),
}},
}
}
return CallToolResult{
Content: []Content{{
Type: ContentTypeText,
Text: some(string(resp.Body())),
}},
}
}
func gistGet(apiKey, gistId string) CallToolResult {
url := fmt.Sprintf("https://api.github.com/gists/%s", gistId)
req := pdk.NewHTTPRequest(pdk.MethodGet, url)
req.SetHeader("Authorization", fmt.Sprintf("token %s", apiKey))
req.SetHeader("Content-Type", "application/json")
req.SetHeader("Accept", "application/vnd.github+json")
req.SetHeader("User-Agent", "github-mcpx-servlet")
resp := req.Send()
if resp.Status() != 201 {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(fmt.Sprintf("Failed to create branch: %d %s", resp.Status(), string(resp.Body()))),
}},
}
}
return CallToolResult{
Content: []Content{{
Type: ContentTypeText,
Text: some(string(resp.Body())),
}},
}
}
func gistDelete(apiKey, gistId string) CallToolResult {
url := fmt.Sprintf("https://api.github.com/gists/%s", gistId)
req := pdk.NewHTTPRequest(pdk.MethodDelete, url)
req.SetHeader("Authorization", fmt.Sprintf("token %s", apiKey))
req.SetHeader("Content-Type", "application/json")
req.SetHeader("Accept", "application/vnd.github+json")
req.SetHeader("User-Agent", "github-mcpx-servlet")
resp := req.Send()
if resp.Status() != 201 {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(fmt.Sprintf("Failed to create branch: %d %s", resp.Status(), string(resp.Body()))),
}},
}
}
return CallToolResult{
Content: []Content{{
Type: ContentTypeText,
Text: some(string(resp.Body())),
}},
}
}
```
--------------------------------------------------------------------------------
/templates/plugins/go/exports.go:
--------------------------------------------------------------------------------
```go
package main
import (
pdk "github.com/extism/go-pdk"
)
//export call_tool
func _CallTool() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "CallTool: getting JSON input")
var input CallToolRequest
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "CallTool: calling implementation function")
output, err := CallTool(input)
if err != nil {
pdk.SetError(err)
return -1
}
if output == nil {
pdk.SetErrorString("CallTool: output is nil")
return -1
}
pdk.Log(pdk.LogDebug, "CallTool: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "CallTool: returning")
return 0
}
//export complete
func _Complete() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "Complete: getting JSON input")
var input CompleteRequest
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "Complete: calling implementation function")
output, err := Complete(input)
if err != nil {
pdk.SetError(err)
return -1
}
if output == nil {
pdk.SetErrorString("Complete: output is nil")
return -1
}
pdk.Log(pdk.LogDebug, "Complete: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "Complete: returning")
return 0
}
//export get_prompt
func _GetPrompt() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "GetPrompt: getting JSON input")
var input GetPromptRequest
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "GetPrompt: calling implementation function")
output, err := GetPrompt(input)
if err != nil {
pdk.SetError(err)
return -1
}
if output == nil {
pdk.SetErrorString("GetPrompt: output is nil")
return -1
}
pdk.Log(pdk.LogDebug, "GetPrompt: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "GetPrompt: returning")
return 0
}
//export list_prompts
func _ListPrompts() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "ListPrompts: getting JSON input")
var input ListPromptsRequest
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "ListPrompts: calling implementation function")
output, err := ListPrompts(input)
if err != nil {
pdk.SetError(err)
return -1
}
if output == nil {
pdk.SetErrorString("ListPrompts: output is nil")
return -1
}
pdk.Log(pdk.LogDebug, "ListPrompts: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "ListPrompts: returning")
return 0
}
//export list_resource_templates
func _ListResourceTemplates() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "ListResourceTemplates: getting JSON input")
var input ListResourceTemplatesRequest
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "ListResourceTemplates: calling implementation function")
output, err := ListResourceTemplates(input)
if err != nil {
pdk.SetError(err)
return -1
}
if output == nil {
pdk.SetErrorString("ListResourceTemplates: output is nil")
return -1
}
pdk.Log(pdk.LogDebug, "ListResourceTemplates: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "ListResourceTemplates: returning")
return 0
}
//export list_resources
func _ListResources() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "ListResources: getting JSON input")
var input ListResourcesRequest
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "ListResources: calling implementation function")
output, err := ListResources(input)
if err != nil {
pdk.SetError(err)
return -1
}
if output == nil {
pdk.SetErrorString("ListResources: output is nil")
return -1
}
pdk.Log(pdk.LogDebug, "ListResources: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "ListResources: returning")
return 0
}
//export list_tools
func _ListTools() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "ListTools: getting JSON input")
var input ListToolsRequest
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "ListTools: calling implementation function")
output, err := ListTools(input)
if err != nil {
pdk.SetError(err)
return -1
}
if output == nil {
pdk.SetErrorString("ListTools: output is nil")
return -1
}
pdk.Log(pdk.LogDebug, "ListTools: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "ListTools: returning")
return 0
}
//export on_roots_list_changed
func _OnRootsListChanged() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "OnRootsListChanged: getting JSON input")
var input PluginNotificationContext
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "OnRootsListChanged: calling implementation function")
err = OnRootsListChanged(input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "OnRootsListChanged: returning")
return 0
}
//export read_resource
func _ReadResource() int32 {
var err error
_ = err
pdk.Log(pdk.LogDebug, "ReadResource: getting JSON input")
var input ReadResourceRequest
err = pdk.InputJSON(&input)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "ReadResource: calling implementation function")
output, err := ReadResource(input)
if err != nil {
pdk.SetError(err)
return -1
}
if output == nil {
pdk.SetErrorString("ReadResource: output is nil")
return -1
}
pdk.Log(pdk.LogDebug, "ReadResource: setting JSON output")
err = pdk.OutputJSON(output)
if err != nil {
pdk.SetError(err)
return -1
}
pdk.Log(pdk.LogDebug, "ReadResource: returning")
return 0
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/github/main.go:
--------------------------------------------------------------------------------
```go
// Note: run `go doc -all` in this package to see all of the types and functions available.
// ./pdk.gen.go contains the domain types from the host where your plugin will run.
package main
import (
"fmt"
"github.com/extism/go-pdk"
)
// Called when the tool is invoked.
// If you support multiple tools, you must switch on the input.params.name to detect which tool is being called.
// The name will match one of the tool names returned from "describe".
// It takes CallToolRequest as input (The incoming tool request from the LLM)
// And returns CallToolResult (The servlet's response to the given tool call)
func Call(input CallToolRequest) (CallToolResult, error) {
apiKey, ok := pdk.GetConfig("api-key")
if !ok {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some("No api-key configured"),
}},
}, nil
}
args := input.Params.Arguments.(map[string]interface{})
pdk.Log(pdk.LogDebug, fmt.Sprint("Args: ", args))
switch input.Params.Name {
case ListIssuesTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
return issueList(apiKey, owner, repo, args)
case GetIssueTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
issue, _ := args["issue"].(float64)
return issueGet(apiKey, owner, repo, int(issue))
case AddIssueCommentTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
issue, _ := args["issue"].(float64)
body, _ := args["body"].(string)
return issueAddComment(apiKey, owner, repo, int(issue), body)
case CreateIssueTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
data := issueFromArgs(args)
return issueCreate(apiKey, owner, repo, data)
case UpdateIssueTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
issue, _ := args["issue"].(float64)
data := issueFromArgs(args)
return issueUpdate(apiKey, owner, repo, int(issue), data)
case GetFileContentsTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
path, _ := args["path"].(string)
branch, _ := args["branch"].(string)
res := filesGetContents(apiKey, owner, repo, path, &branch)
return res, nil
case CreateOrUpdateFileTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
path, _ := args["path"].(string)
file := fileCreateFromArgs(args)
return filesCreateOrUpdate(apiKey, owner, repo, path, file)
case CreateBranchTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
from, _ := args["branch"].(string)
var maybeBranch *string
if branch, ok := args["from_branch"].(string); ok {
maybeBranch = &branch
}
return branchCreate(apiKey, owner, repo, from, maybeBranch), nil
case ListPullRequestsTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
return pullRequestList(apiKey, owner, repo, args)
case CreatePullRequestTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
pr := branchPullRequestSchemaFromArgs(args)
return branchCreatePullRequest(apiKey, owner, repo, pr), nil
case PushFilesTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
branch, _ := args["branch"].(string)
message, _ := args["message"].(string)
files := filePushFromArgs(args)
return filesPush(apiKey, owner, repo, branch, message, files), nil
case ListReposTool.Name:
owner, _ := args["owner"].(string)
return reposList(apiKey, owner, args)
case GetRepositoryCollaboratorsTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
return reposGetCollaborators(apiKey, owner, repo, args)
case GetRepositoryContributorsTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
return reposGetContributors(apiKey, owner, repo, args)
case GetRepositoryDetailsTool.Name:
owner, _ := args["owner"].(string)
repo, _ := args["repo"].(string)
return reposGetDetails(apiKey, owner, repo)
case CreateGistTool.Name:
description, _ := args["description"].(string)
files, _ := args["files"].(map[string]any)
return gistCreate(apiKey, description, files), nil
case GetGistTool.Name:
gistId, _ := args["gist_id"].(string)
return gistGet(apiKey, gistId), nil
case UpdateGistTool.Name:
gistId, _ := args["gist_id"].(string)
description, _ := args["description"].(string)
files, _ := args["files"].(map[string]any)
return gistUpdate(apiKey, gistId, description, files), nil
case DeleteGistTool.Name:
gistId, _ := args["gist_id"].(string)
return gistDelete(apiKey, gistId), nil
default:
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some("Unknown tool " + input.Params.Name),
}},
}, nil
}
}
func Describe() (ListToolsResult, error) {
toolsets := [][]ToolDescription{
IssueTools,
FileTools,
BranchTools,
RepoTools,
GistTools,
}
tools := []ToolDescription{}
for _, toolset := range toolsets {
tools = append(tools, toolset...)
}
// Ensure each tool's InputSchema has a required field
for i := range tools {
// Check if InputSchema is a map[string]interface{}
if schema, ok := tools[i].InputSchema.(map[string]interface{}); ok {
// Check if required field is missing
if _, exists := schema["required"]; !exists {
// Add an empty required array if it doesn't exist
schema["required"] = []string{}
tools[i].InputSchema = schema
}
}
}
return ListToolsResult{
Tools: tools,
}, nil
}
func some[T any](t T) *T {
return &t
}
type SchemaProperty struct {
Type string `json:"type"`
Description string `json:"description,omitempty"`
AdditionalProperties *schema `json:"additionalProperties,omitempty"`
Items *schema `json:"items,omitempty"`
}
func prop(tpe, description string) SchemaProperty {
return SchemaProperty{Type: tpe, Description: description}
}
func arrprop(tpe, description, itemstpe string) SchemaProperty {
items := schema{"type": itemstpe}
return SchemaProperty{Type: tpe, Description: description, Items: &items}
}
type schema = map[string]interface{}
type props = map[string]SchemaProperty
```
--------------------------------------------------------------------------------
/examples/plugins/v1/maven/src/lib.rs:
--------------------------------------------------------------------------------
```rust
mod pdk;
use std::collections::BTreeMap;
use extism_pdk::*;
use json::Value;
use pdk::types::{
CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
};
use serde_json::json;
pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
match input.params.name.as_str() {
"mvn_fetch_deps" => mvn_fetch_deps(input),
_ => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Unknown tool: {}", input.params.name)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
}
fn mvn_fetch_deps(input: CallToolRequest) -> Result<CallToolResult, Error> {
use quick_xml::Reader;
use quick_xml::events::Event;
use serde_json::json;
let args = input.params.arguments.unwrap_or_default();
let group = match args.get("group") {
Some(Value::String(s)) => s,
_ => {
return Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Missing 'group' argument".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
});
}
};
let artifact = match args.get("artifact") {
Some(Value::String(s)) => s,
_ => {
return Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Missing 'artifact' argument".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
});
}
};
let version = match args.get("version") {
Some(Value::String(s)) => s,
_ => {
return Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Missing 'version' argument".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
});
}
};
let group_path = group.replace('.', "/");
let pom_url = format!(
"https://repo1.maven.org/maven2/{}/{}/{}/{}-{}.pom",
group_path, artifact, version, artifact, version
);
let mut req = HttpRequest {
url: pom_url.clone(),
headers: BTreeMap::new(),
method: Some("GET".to_string()),
};
req.headers
.insert("User-Agent".to_string(), "maven-plugin/1.0".to_string());
let res = match http::request::<()>(&req, None) {
Ok(r) => r,
Err(e) => {
return Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Failed to fetch POM: {}", e)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
});
}
};
let body = res.body();
let xml = String::from_utf8_lossy(body.as_slice());
// Parse dependencies
let mut reader = Reader::from_str(&xml);
reader.trim_text(true);
let mut buf = Vec::new();
let mut dependencies = Vec::new();
let mut in_dependencies = false;
let mut current = serde_json::Map::new();
let mut current_tag = String::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
if tag == "dependencies" {
in_dependencies = true;
} else if in_dependencies && tag == "dependency" {
current = serde_json::Map::new();
} else if in_dependencies {
current_tag = tag;
}
}
Ok(Event::End(ref e)) => {
let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
if tag == "dependencies" {
in_dependencies = false;
} else if in_dependencies && tag == "dependency" {
dependencies.push(json!(current));
} else if in_dependencies {
current_tag.clear();
}
}
Ok(Event::Text(e)) => {
if in_dependencies && !current_tag.is_empty() {
current.insert(current_tag.clone(), json!(e.unescape().unwrap_or_default()));
}
}
Ok(Event::Eof) => break,
Err(_) => break,
_ => {}
}
buf.clear();
}
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(json!({"dependencies": dependencies}).to_string()),
mime_type: Some("application/json".to_string()),
r#type: ContentType::Text,
data: None,
}],
})
}
pub(crate) fn describe() -> Result<ListToolsResult, Error> {
Ok(ListToolsResult{
tools: vec![
ToolDescription {
name: "mvn_fetch_deps".into(),
description: "Fetches the dependencies of a Maven package by group, artifact, and version from Maven Central.".into(),
input_schema: json!({
"type": "object",
"properties": {
"group": {
"type": "string",
"description": "The Maven groupId",
},
"artifact": {
"type": "string",
"description": "The Maven artifactId",
},
"version": {
"type": "string",
"description": "The Maven version",
},
},
"required": ["group", "artifact", "version"],
})
.as_object()
.unwrap()
.clone(),
},
],
})
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/gomodule/src/lib.rs:
--------------------------------------------------------------------------------
```rust
mod pdk;
use std::collections::BTreeMap;
use extism_pdk::*;
use json::Value;
use pdk::types::{
CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
};
use serde_json::json;
pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
match input.params.name.as_str() {
"gomodule_latest_version" => latest_version(input),
"gomodule_info" => module_info(input),
_ => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Unknown tool: {}", input.params.name)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
}
fn module_info(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.unwrap_or_default();
if let Some(Value::String(module_names)) = args.get("module_names") {
let module_names: Vec<&str> = module_names.split(',').map(|s| s.trim()).collect();
let mut results = Vec::new();
for module_name in module_names {
let mut req = HttpRequest {
url: format!("https://proxy.golang.org/{}/@latest", module_name),
headers: BTreeMap::new(),
method: Some("GET".to_string()),
};
req.headers
.insert("User-Agent".to_string(), "hyper-mcp/1.0".to_string());
let res = http::request::<()>(&req, None)?;
let body = res.body();
let json_str = String::from_utf8_lossy(body.as_slice());
let json: serde_json::Value = serde_json::from_str(&json_str)?;
// TODO: figure out how to get module license
results.push(json);
}
if !results.is_empty() {
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(serde_json::to_string(&results)?),
mime_type: Some("text/plain".to_string()),
r#type: ContentType::Text,
data: None,
}],
})
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Failed to get module information".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide module names".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
fn latest_version(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.unwrap_or_default();
if let Some(Value::String(module_names)) = args.get("module_names") {
let module_names: Vec<&str> = module_names.split(',').map(|s| s.trim()).collect();
let mut results = BTreeMap::new();
for module_name in module_names {
let mut req = HttpRequest {
url: format!("https://proxy.golang.org/{}/@latest", module_name),
headers: BTreeMap::new(),
method: Some("GET".to_string()),
};
req.headers
.insert("User-Agent".to_string(), "hyper-mcp/1.0".to_string());
let res = http::request::<()>(&req, None)?;
let body = res.body();
let json_str = String::from_utf8_lossy(body.as_slice());
let json: serde_json::Value = serde_json::from_str(&json_str)?;
if let Some(version) = json["Version"].as_str() {
results.insert(module_name.to_string(), version.to_string());
}
}
if !results.is_empty() {
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(serde_json::to_string(&results)?),
mime_type: Some("text/plain".to_string()),
r#type: ContentType::Text,
data: None,
}],
})
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Failed to get latest versions".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide module names".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
pub(crate) fn describe() -> Result<ListToolsResult, Error> {
Ok(ListToolsResult {
tools: vec![
ToolDescription {
name: "gomodule_latest_version".into(),
description: "Fetches the latest version of multiple Go modules. Assume it's github.com if not specified".into(),
input_schema: json!({
"type": "object",
"properties": {
"module_names": {
"type": "string",
"description": "Comma-separated list of Go module names to get the latest versions for",
},
},
"required": ["module_names"],
})
.as_object()
.unwrap()
.clone(),
},
ToolDescription {
name: "gomodule_info".into(),
description: "Fetches detailed information about multiple Go modules. Assume it's github.com if not specified".into(),
input_schema: json!({
"type": "object",
"properties": {
"module_names": {
"type": "string",
"description": "Comma-separated list of Go module names to get information for",
},
},
"required": ["module_names"],
})
.as_object()
.unwrap()
.clone(),
},
],
})
}
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
name: Release
on:
push:
tags:
- "v*" # Match tags like v1.0.0, v2.1.3 etc.
env:
CARGO_TERM_COLOR: always
REGISTRY: ghcr.io
jobs:
build-oci-images:
strategy:
matrix:
include:
- os: ubuntu-24.04 # For amd64, consistent with nightly
arch: amd64
- os: ubuntu-24.04-arm # For arm64
arch: arm64
runs-on: ${{ matrix.os }}
permissions:
contents: write
packages: write
id-token: write # needed for keyless signing
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
submodules: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Log in to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# tag images with both git tag & latest
- name: Build and push hyper-mcp arch specific image
run: |
TAG=${GITHUB_REF#refs/tags/}
hyper_mcp_image_arch="${{ env.REGISTRY }}/${{ github.repository_owner }}/hyper-mcp:$TAG-${{ matrix.arch }}"
echo "Building and tagging arch specific image: $hyper_mcp_image_arch for ${{ matrix.arch }}"
docker build -t $hyper_mcp_image_arch .
docker push $hyper_mcp_image_arch
cosign sign --yes $hyper_mcp_image_arch
- name: Build and push plugin images (on amd64 only)
if: matrix.arch == 'amd64'
run: |
TAG=${GITHUB_REF#refs/tags/}
for plugin in examples/plugins/v{1,2}/*/; do
plugin_name=$(basename $plugin)
plugin_base_image="${{ env.REGISTRY }}/${{ github.repository_owner }}/${plugin_name}-plugin"
echo "Building and tagging plugin: $plugin_name as $plugin_base_image:$TAG and $plugin_base_image:latest"
docker build -t $plugin_base_image:$TAG -t $plugin_base_image:latest $plugin
docker push $plugin_base_image:$TAG
docker push $plugin_base_image:latest
cosign sign --yes $plugin_base_image:$TAG
cosign sign --yes $plugin_base_image:latest
done
create-multiarch-manifests:
needs: build-oci-images
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # needed for keyless signing
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Log in to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push multi-arch manifest for hyper-mcp
run: |
TAG=${GITHUB_REF#refs/tags/}
hyper_mcp_base_image="${{ env.REGISTRY }}/${{ github.repository_owner }}/hyper-mcp"
echo "Creating multi-arch manifest for $hyper_mcp_base_image:$TAG"
docker buildx imagetools create \
-t $hyper_mcp_base_image:$TAG \
$hyper_mcp_base_image:$TAG-amd64 \
$hyper_mcp_base_image:$TAG-arm64
cosign sign --yes $hyper_mcp_base_image:$TAG
echo "Creating multi-arch manifest for $hyper_mcp_base_image:latest"
docker buildx imagetools create \
-t $hyper_mcp_base_image:latest \
$hyper_mcp_base_image:$TAG-amd64 \
$hyper_mcp_base_image:$TAG-arm64
cosign sign --yes $hyper_mcp_base_image:latest
build-binaries:
strategy:
matrix:
include:
- os: ubuntu-latest
arch: x86_64
target: x86_64-unknown-linux-gnu
- os: ubuntu-24.04-arm
arch: aarch64
target: aarch64-unknown-linux-gnu
- os: macos-latest
arch: aarch64
target: aarch64-apple-darwin
runs-on: ${{ matrix.os }}
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- run: cargo install cargo-auditable
- name: Install target
run: rustup target add ${{ matrix.target }}
- name: Build
run: cargo auditable build --target ${{ matrix.target }} --release
- name: Create archives and checksums
run: |
mkdir -p dist/${{ matrix.target }}
cp target/${{ matrix.target }}/release/hyper-mcp dist/${{ matrix.target }}/
cd dist/${{ matrix.target }} && tar -czf ../hyper-mcp-${{ matrix.target }}.tar.gz hyper-mcp
cd ..
{
echo "hyper-mcp-${{ matrix.target }}.tar.gz:"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum hyper-mcp-${{ matrix.target }}.tar.gz
else
shasum -a 256 hyper-mcp-${{ matrix.target }}.tar.gz
fi
} > checksums.txt
- name: Create GitHub Release
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
with:
tag_name: ${{ github.ref_name }}
name: Release ${{ github.ref_name }}
draft: false
prerelease: false
files: |
dist/hyper-mcp-${{ matrix.target }}.tar.gz
dist/checksums.txt
body: |
Final release for `${{ github.ref_name }}`.
Included:
- hyper-mcp binaries for Linux & macOS
- hyper-mcp container image: `ghcr.io/${{ github.repository_owner }}/hyper-mcp:${{ github.ref_name }}`
- Plugin images: `ghcr.io/${{ github.repository_owner }}/<plugin-name>-plugin:${{ github.ref_name }}`
All container images are signed with Cosign. Verify with:
```bash
cosign verify \
--certificate-identity "https://github.com/tuananh/hyper-mcp/.github/workflows/release.yml@refs/tags/${{ github.ref_name }}" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/tuananh/hyper-mcp:${{ github.ref_name }}
```
```
--------------------------------------------------------------------------------
/examples/plugins/v1/tool-list-changed/src/pdk.rs:
--------------------------------------------------------------------------------
```rust
// THIS FILE WAS GENERATED BY `xtp-rust-bindgen`. DO NOT EDIT.
#![allow(non_snake_case)]
#![allow(unused_macros)]
use extism_pdk::*;
#[allow(unused)]
fn panic_if_key_missing() -> ! {
panic!("missing key");
}
pub(crate) mod internal {
pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
let err = format!("{:?}", e);
let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
unsafe {
extism_pdk::extism::error_set(mem.offset());
}
-1
}
}
#[allow(unused)]
macro_rules! try_input {
() => {{
let x = extism_pdk::input();
match x {
Ok(x) => x,
Err(e) => return internal::return_error(e),
}
}};
}
#[allow(unused)]
macro_rules! try_input_json {
() => {{
let x = extism_pdk::input();
match x {
Ok(extism_pdk::Json(x)) => x,
Err(e) => return internal::return_error(e),
}
}};
}
use base64_serde::base64_serde_type;
base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
mod exports {
use super::*;
#[unsafe(no_mangle)]
pub extern "C" fn call() -> i32 {
let ret =
crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn describe() -> i32 {
let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
}
pub mod types {
use super::*;
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolRequest {
#[serde(rename = "method")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub method: Option<String>,
#[serde(rename = "params")]
pub params: types::Params,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolResult {
#[serde(rename = "content")]
pub content: Vec<types::Content>,
/// Whether the tool call ended in an error.
///
/// If not set, this is assumed to be false (the call was successful).
#[serde(rename = "isError")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub is_error: Option<bool>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Content {
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<types::TextAnnotation>,
/// The base64-encoded image data.
#[serde(rename = "data")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub data: Option<String>,
/// The MIME type of the image. Different providers may support different image types.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text content of the message.
#[serde(rename = "text")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub text: Option<String>,
#[serde(rename = "type")]
pub r#type: types::ContentType,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum ContentType {
#[default]
#[serde(rename = "text")]
Text,
#[serde(rename = "image")]
Image,
#[serde(rename = "resource")]
Resource,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ListToolsResult {
/// The list of ToolDescription objects provided by this servlet.
#[serde(rename = "tools")]
pub tools: Vec<types::ToolDescription>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Params {
#[serde(rename = "arguments")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(rename = "name")]
pub name: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum Role {
#[default]
#[serde(rename = "assistant")]
Assistant,
#[serde(rename = "user")]
User,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextAnnotation {
/// Describes who the intended customer of this object or data is.
///
/// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
#[serde(rename = "audience")]
pub audience: Vec<types::Role>,
/// Describes how important this data is for operating the server.
///
/// A value of 1 means "most important," and indicates that the data is
/// effectively required, while 0 means "least important," and indicates that
/// the data is entirely optional.
#[serde(rename = "priority")]
pub priority: f32,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ToolDescription {
/// A description of the tool
#[serde(rename = "description")]
pub description: String,
/// The JSON schema describing the argument input
#[serde(rename = "inputSchema")]
pub input_schema: serde_json::Map<String, serde_json::Value>,
/// The name of the tool. It should match the plugin / binding name.
#[serde(rename = "name")]
pub name: String,
}
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/arxiv/src/lib.rs:
--------------------------------------------------------------------------------
```rust
mod pdk;
use chrono::{DateTime, Utc};
use extism_pdk::*;
use pdk::types::{
CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, Role, TextAnnotation,
ToolDescription,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Debug, Serialize, Deserialize)]
struct Paper {
paper_id: String,
title: String,
authors: Vec<String>,
abstract_text: String,
url: String,
pdf_url: String,
published_date: DateTime<Utc>,
updated_date: DateTime<Utc>,
source: String,
categories: Vec<String>,
keywords: Vec<String>,
doi: String,
}
pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
match input.params.name.as_str() {
"arxiv_search" => search(input),
"arxiv_download_pdf" => download_pdf(input),
_ => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Unknown tool: {}", input.params.name)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
}
fn search(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.unwrap_or_default();
let query = match args.get("query") {
Some(v) if v.is_string() => v.as_str().unwrap(),
_ => return Err(Error::msg("query parameter is required")),
};
let max_results = args
.get("max_results")
.and_then(|v| v.as_u64())
.unwrap_or(10);
let req = HttpRequest {
url: format!(
"http://export.arxiv.org/api/query?search_query={}&max_results={}&sortBy=submittedDate&sortOrder=descending",
urlencoding::encode(query),
max_results
),
headers: [(
"User-Agent".to_string(),
"hyper-mcp/1.0 (https://github.com/tuananh/hyper-mcp)".to_string(),
)]
.into_iter()
.collect(),
method: Some("GET".to_string()),
};
let res = http::request::<()>(&req, None)?;
let body = res.body();
let xml = String::from_utf8_lossy(body.as_slice());
let feed = match feed_rs::parser::parse(xml.as_bytes()) {
Ok(feed) => feed,
Err(e) => return Err(Error::msg(format!("Failed to parse arXiv feed: {}", e))),
};
let mut papers = Vec::new();
for entry in feed.entries {
let paper_id = entry.id.split("/abs/").last().unwrap_or("").to_string();
let authors = entry
.authors
.iter()
.map(|author| author.name.clone())
.collect();
let categories = entry
.categories
.iter()
.map(|cat| cat.term.clone())
.collect();
papers.push(Paper {
paper_id,
title: entry.title.map(|t| t.content).unwrap_or_default(),
authors,
abstract_text: entry.content.and_then(|c| c.body).unwrap_or_default(),
url: entry
.links
.iter()
.find(|l| l.rel == Some("alternate".to_string()))
.map(|l| l.href.clone())
.unwrap_or_default(),
pdf_url: entry
.links
.iter()
.find(|l| l.media_type.as_deref() == Some("application/pdf"))
.map(|l| l.href.clone())
.unwrap_or_default(),
published_date: entry.published.unwrap_or_default(),
updated_date: entry.updated.unwrap_or_default(),
source: "arxiv".to_string(),
categories,
keywords: Vec::new(),
doi: String::new(),
});
}
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(serde_json::to_string(&papers)?),
mime_type: Some("application/json".to_string()),
r#type: ContentType::Text,
data: None,
}],
})
}
fn download_pdf(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.unwrap_or_default();
let paper_id = match args.get("paper_id") {
Some(v) if v.is_string() => v.as_str().unwrap(),
_ => return Err(Error::msg("paper_id parameter is required")),
};
// Get the path parameter with default to /tmp
let save_path = args
.get("save_path")
.and_then(|v| v.as_str())
.unwrap_or("/tmp");
// Clean up the paper ID in case it contains the full URL
let clean_paper_id = if paper_id.contains("/") {
paper_id.split('/').next_back().unwrap_or(paper_id)
} else {
paper_id
};
let url = format!("https://arxiv.org/pdf/{}", clean_paper_id);
let req = HttpRequest {
url,
headers: [
(
"User-Agent".to_string(),
"Mozilla/5.0 (compatible; hyper-mcp/1.0)".to_string(),
),
("Accept".to_string(), "application/pdf".to_string()),
]
.into_iter()
.collect(),
method: Some("GET".to_string()),
};
let res = match http::request::<()>(&req, None) {
Ok(r) => r,
Err(e) => return Err(Error::msg(format!("HTTP request failed: {}", e))),
};
let pdf_data = res.body();
if pdf_data.is_empty() {
return Err(Error::msg("Received empty PDF data from arXiv"));
}
let file_path = format!("{}/{}.pdf", save_path.trim_end_matches('/'), clean_paper_id);
match std::fs::write(&file_path, &pdf_data) {
Ok(_) => (),
Err(e) => {
return Err(Error::msg(format!(
"Failed to write PDF to {}: {}",
file_path, e
)));
}
}
// let pdf_base64 = base64::engine::general_purpose::STANDARD.encode(pdf_data);
// TODO: actually return a resource
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: Some(TextAnnotation {
audience: vec![Role::User, Role::Assistant],
priority: 1.0,
}),
text: Some(format!("PDF saved to: {}", file_path)),
mime_type: None,
data: None,
r#type: ContentType::Text,
}],
})
}
pub(crate) fn describe() -> Result<ListToolsResult, Error> {
Ok(ListToolsResult {
tools: vec![
ToolDescription {
name: "arxiv_search".into(),
description: "Search for papers on arXiv".into(),
input_schema: json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query",
},
"max_results": {
"type": "integer",
"description": "Maximum number of results to return (default: 10)",
}
},
"required": ["query"],
})
.as_object()
.unwrap()
.clone(),
},
ToolDescription {
name: "arxiv_download_pdf".into(),
description: "Download a paper's PDF from arXiv".into(),
input_schema: json!({
"type": "object",
"properties": {
"paper_id": {
"type": "string",
"description": "The arXiv paper ID",
},
"save_path": {
"type": "string",
"description": "Path to save the PDF file (default: /tmp)",
}
},
"required": ["paper_id"],
})
.as_object()
.unwrap()
.clone(),
},
],
})
}
```
--------------------------------------------------------------------------------
/.github/workflows/nightly.yml:
--------------------------------------------------------------------------------
```yaml
name: Nightly Release
on:
schedule:
- cron: "0 17 * * *" # midnight GMT+7
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
build-oci-images:
strategy:
matrix:
include:
- os: ubuntu-24.04
arch: amd64
- os: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.os }}
permissions:
contents: write
packages: write
id-token: write # needed for keyless signing
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
submodules: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Log in to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to DockerHub Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build and push hyper-mcp
run: |
echo "Building hyper-mcp image"
ghcr_image="ghcr.io/${{ github.repository_owner }}/hyper-mcp:nightly-${{ matrix.arch }}"
dockerhub_image="docker.io/tuananh/hyper-mcp:nightly-${{ matrix.arch }}"
docker build -t $ghcr_image -t $dockerhub_image .
docker push $ghcr_image
docker push $dockerhub_image
cosign sign --yes $ghcr_image
cosign sign --yes $dockerhub_image
# we dont need to build multi-arch plugin images as they are wasm32-wasip1
# so we can just build amd64 and push it to the registry
- name: Build and push plugin images
if: matrix.arch == 'amd64'
run: |
for plugin in examples/plugins/v{1,2}/*/; do
plugin_name=$(basename $plugin)
echo "Building plugin: $plugin_name"
image_name="ghcr.io/${{ github.repository_owner }}/${plugin_name}-plugin:nightly"
docker build -t $image_name $plugin
docker push $image_name
cosign sign --yes $image_name
done
# do this before we build nightly binaries
prepare-nightly-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
- name: Set nightly tag to latest main
run: |
git fetch origin main
git tag -f nightly origin/main
git push -f origin nightly
- name: Delete existing nightly release
run: gh release delete nightly --yes || true
build-nightly-binaries:
needs: prepare-nightly-release
strategy:
matrix:
include:
- os: ubuntu-24.04
arch: x86_64
target: x86_64-unknown-linux-gnu
ext: ""
- os: ubuntu-24.04-arm
arch: aarch64
target: aarch64-unknown-linux-gnu
ext: ""
- os: macos-latest
arch: aarch64
target: aarch64-apple-darwin
ext: ""
- os: windows-latest
arch: x86_64
target: x86_64-pc-windows-msvc
ext: ".exe"
runs-on: ${{ matrix.os }}
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- run: cargo install cargo-auditable
- name: Install compilation targets
run: rustup target add ${{ matrix.target }}
- name: Build
run: cargo auditable build --target ${{ matrix.target }} --release
- name: Install zip (Windows only)
if: runner.os == 'Windows'
run: choco install zip -y
- name: Create archives and checksums
shell: bash
run: |
mkdir -p dist/${{ matrix.target }}
bin_name="hyper-mcp${{ matrix.ext }}"
cp target/${{ matrix.target }}/release/$bin_name dist/${{ matrix.target }}/
cd dist/${{ matrix.target }}
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
zip ../hyper-mcp-${{ matrix.target }}.zip $bin_name
else
tar -czf ../hyper-mcp-${{ matrix.target }}.tar.gz $bin_name
fi
cd ..
{
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
file="hyper-mcp-${{ matrix.target }}.zip"
else
file="hyper-mcp-${{ matrix.target }}.tar.gz"
fi
echo "$file:"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum $file
else
shasum -a 256 $file
fi
} > checksums-${{ matrix.target }}.txt
- name: Create new nightly release
id: create_release
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
with:
tag_name: nightly
name: Nightly build
draft: false
prerelease: true
files: |
dist/hyper-mcp-${{ matrix.target }}.tar.gz
dist/hyper-mcp-${{ matrix.target }}.zip
dist/checksums-${{ matrix.target }}.txt
body: |
Nightly build from `main` branch.
This release includes:
- hyper-mcp binaries for Linux & macOS
- hyper-mcp container image: `ghcr.io/${{ github.repository_owner }}/hyper-mcp:nightly`
- Plugin images: `ghcr.io/${{ github.repository_owner }}/<plugin-name>-plugin:nightly`
All container images are signed with Cosign. Verify the image like this:
```bash
cosign verify \
--certificate-identity "https://github.com/tuananh/hyper-mcp/.github/workflows/nightly.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/tuananh/hyper-mcp:nightly
```
create-multiarch-manifests:
needs: build-oci-images
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
id-token: write # needed for keyless signing
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Install cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- name: Log in to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to DockerHub Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Create and push multi-arch nightly tags
run: |
# Main image
docker buildx imagetools create \
-t ghcr.io/${{ github.repository_owner }}/hyper-mcp:nightly \
ghcr.io/${{ github.repository_owner }}/hyper-mcp:nightly-amd64 \
ghcr.io/${{ github.repository_owner }}/hyper-mcp:nightly-arm64
cosign sign --yes ghcr.io/${{ github.repository_owner }}/hyper-mcp:nightly
# DockerHub
docker buildx imagetools create \
-t tuananh/hyper-mcp:nightly \
tuananh/hyper-mcp:nightly-amd64 \
tuananh/hyper-mcp:nightly-arm64
cosign sign --yes tuananh/hyper-mcp:nightly
```
--------------------------------------------------------------------------------
/examples/plugins/v1/crates-io/src/lib.rs:
--------------------------------------------------------------------------------
```rust
mod pdk;
use std::collections::BTreeMap;
use extism_pdk::*;
use json::Value;
use pdk::types::{
CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
};
use serde_json::json;
pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
match input.params.name.as_str() {
"crates_io_latest_version" => latest_version(input),
"crates_io_crate_info" => crate_info(input),
_ => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Unknown tool: {}", input.params.name)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
}
fn crate_info(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.unwrap_or_default();
if let Some(Value::String(crate_names)) = args.get("crate_names") {
let crate_names: Vec<&str> = crate_names.split(',').map(|s| s.trim()).collect();
let mut results = Vec::new();
for crate_name in crate_names {
// Create HTTP request to crates.io API
let mut req = HttpRequest {
url: format!("https://crates.io/api/v1/crates/{}", crate_name),
headers: BTreeMap::new(),
method: Some("GET".to_string()),
};
// Add a user agent header to be polite
req.headers
.insert("User-Agent".to_string(), "crates-io-tool/1.0".to_string());
// Perform the request
let res = http::request::<()>(&req, None)?;
// Convert response body to string
let body = res.body();
let json_str = String::from_utf8_lossy(body.as_slice());
// Parse the JSON response
let json: serde_json::Value = serde_json::from_str(&json_str)?;
if let Some(crate_info) = json["crate"].as_object() {
// Extract relevant information with null checks
let info = json!({
"name": crate_info.get("name").and_then(|v| v.as_str()),
"description": crate_info.get("description").and_then(|v| v.as_str()),
"latest_version": crate_info.get("max_version").and_then(|v| v.as_str()),
"downloads": crate_info.get("downloads").and_then(|v| v.as_i64()),
"repository": crate_info.get("repository").and_then(|v| v.as_str()),
"documentation": crate_info.get("documentation").and_then(|v| v.as_str()),
"homepage": crate_info.get("homepage").and_then(|v| v.as_str()),
"keywords": crate_info.get("keywords").and_then(|v| v.as_array()),
"categories": crate_info.get("categories").and_then(|v| v.as_array()),
"license": json["versions"].as_array().and_then(|v| v.first()).and_then(|v| v["license"].as_str()),
"created_at": crate_info.get("created_at").and_then(|v| v.as_str()),
"updated_at": crate_info.get("updated_at").and_then(|v| v.as_str()),
});
results.push(info);
}
}
if !results.is_empty() {
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(serde_json::to_string(&results)?),
mime_type: Some("text/plain".to_string()),
r#type: ContentType::Text,
data: None,
}],
})
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Failed to get crate information".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide crate names".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
fn latest_version(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.unwrap_or_default();
if let Some(Value::String(crate_names)) = args.get("crate_names") {
let crate_names: Vec<&str> = crate_names.split(',').map(|s| s.trim()).collect();
let mut results = BTreeMap::new();
for crate_name in crate_names {
// Create HTTP request to crates.io API
let mut req = HttpRequest {
url: format!("https://crates.io/api/v1/crates/{}", crate_name),
headers: BTreeMap::new(),
method: Some("GET".to_string()),
};
// Add a user agent header to be polite
req.headers
.insert("User-Agent".to_string(), "crates-io-tool/1.0".to_string());
// Perform the request
let res = http::request::<()>(&req, None)?;
// Convert response body to string
let body = res.body();
let json_str = String::from_utf8_lossy(body.as_slice());
// Parse the JSON response
let json: serde_json::Value = serde_json::from_str(&json_str)?;
if let Some(version) = json["crate"]["max_version"].as_str() {
results.insert(crate_name.to_string(), version.to_string());
}
}
if !results.is_empty() {
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(serde_json::to_string(&results)?),
mime_type: Some("text/plain".to_string()),
r#type: ContentType::Text,
data: None,
}],
})
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Failed to get latest versions".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide crate names".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
pub(crate) fn describe() -> Result<ListToolsResult, Error> {
Ok(ListToolsResult {
tools: vec![
ToolDescription {
name: "crates_io_latest_version".into(),
description: "Fetches the latest version of multiple crates from crates.io".into(),
input_schema: json!({
"type": "object",
"properties": {
"crate_names": {
"type": "string",
"description": "Comma-separated list of crate names to get the latest versions for",
},
},
"required": ["crate_names"],
})
.as_object()
.unwrap()
.clone(),
},
ToolDescription {
name: "crates_io_crate_info".into(),
description: "Fetches detailed information about multiple crates from crates.io".into(),
input_schema: json!({
"type": "object",
"properties": {
"crate_names": {
"type": "string",
"description": "Comma-separated list of crate names to get information for",
},
},
"required": ["crate_names"],
})
.as_object()
.unwrap()
.clone(),
},
],
})
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/arxiv/src/pdk.rs:
--------------------------------------------------------------------------------
```rust
#![allow(non_snake_case)]
#![allow(unused_macros)]
use extism_pdk::*;
#[allow(unused)]
fn panic_if_key_missing() -> ! {
panic!("missing key");
}
pub(crate) mod internal {
pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
let err = format!("{:?}", e);
let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
unsafe {
extism_pdk::extism::error_set(mem.offset());
}
-1
}
}
#[allow(unused)]
macro_rules! try_input {
() => {{
let x = extism_pdk::input();
match x {
Ok(x) => x,
Err(e) => return internal::return_error(e),
}
}};
}
#[allow(unused)]
macro_rules! try_input_json {
() => {{
let x = extism_pdk::input();
match x {
Ok(extism_pdk::Json(x)) => x,
Err(e) => return internal::return_error(e),
}
}};
}
use base64_serde::base64_serde_type;
base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
mod exports {
use super::*;
#[unsafe(no_mangle)]
pub extern "C" fn call() -> i32 {
let ret =
crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn describe() -> i32 {
let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
}
pub mod types {
use super::*;
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct BlobResourceContents {
/// A base64-encoded string representing the binary data of the item.
#[serde(rename = "blob")]
pub blob: String,
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolRequest {
#[serde(rename = "method")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub method: Option<String>,
#[serde(rename = "params")]
pub params: types::Params,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolResult {
#[serde(rename = "content")]
pub content: Vec<types::Content>,
/// Whether the tool call ended in an error.
///
/// If not set, this is assumed to be false (the call was successful).
#[serde(rename = "isError")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub is_error: Option<bool>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Content {
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<types::TextAnnotation>,
/// The base64-encoded image data.
#[serde(rename = "data")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub data: Option<String>,
/// The MIME type of the image. Different providers may support different image types.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text content of the message.
#[serde(rename = "text")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub text: Option<String>,
#[serde(rename = "type")]
pub r#type: types::ContentType,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum ContentType {
#[default]
#[serde(rename = "text")]
Text,
#[serde(rename = "image")]
Image,
#[serde(rename = "resource")]
Resource,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ListToolsResult {
/// The list of ToolDescription objects provided by this servlet.
#[serde(rename = "tools")]
pub tools: Vec<types::ToolDescription>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Params {
#[serde(rename = "arguments")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(rename = "name")]
pub name: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum Role {
#[default]
#[serde(rename = "assistant")]
Assistant,
#[serde(rename = "user")]
User,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextAnnotation {
/// Describes who the intended customer of this object or data is.
///
/// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
#[serde(rename = "audience")]
pub audience: Vec<types::Role>,
/// Describes how important this data is for operating the server.
///
/// A value of 1 means "most important," and indicates that the data is
/// effectively required, while 0 means "least important," and indicates that
/// the data is entirely optional.
#[serde(rename = "priority")]
pub priority: f32,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextResourceContents {
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
#[serde(rename = "text")]
pub text: String,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ToolDescription {
/// A description of the tool
#[serde(rename = "description")]
pub description: String,
/// The JSON schema describing the argument input
#[serde(rename = "inputSchema")]
pub input_schema: serde_json::Map<String, serde_json::Value>,
/// The name of the tool. It should match the plugin / binding name.
#[serde(rename = "name")]
pub name: String,
}
}
mod raw_imports {
use super::*;
#[host_fn]
extern "ExtismHost" {}
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/context7/src/pdk.rs:
--------------------------------------------------------------------------------
```rust
#![allow(non_snake_case)]
#![allow(unused_macros)]
use extism_pdk::*;
#[allow(unused)]
fn panic_if_key_missing() -> ! {
panic!("missing key");
}
pub(crate) mod internal {
pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
let err = format!("{:?}", e);
let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
unsafe {
extism_pdk::extism::error_set(mem.offset());
}
-1
}
}
#[allow(unused)]
macro_rules! try_input {
() => {{
let x = extism_pdk::input();
match x {
Ok(x) => x,
Err(e) => return internal::return_error(e),
}
}};
}
#[allow(unused)]
macro_rules! try_input_json {
() => {{
let x = extism_pdk::input();
match x {
Ok(extism_pdk::Json(x)) => x,
Err(e) => return internal::return_error(e),
}
}};
}
use base64_serde::base64_serde_type;
base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
mod exports {
use super::*;
#[unsafe(no_mangle)]
pub extern "C" fn call() -> i32 {
let ret =
crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn describe() -> i32 {
let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
}
pub mod types {
use super::*;
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct BlobResourceContents {
/// A base64-encoded string representing the binary data of the item.
#[serde(rename = "blob")]
pub blob: String,
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolRequest {
#[serde(rename = "method")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub method: Option<String>,
#[serde(rename = "params")]
pub params: types::Params,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolResult {
#[serde(rename = "content")]
pub content: Vec<types::Content>,
/// Whether the tool call ended in an error.
///
/// If not set, this is assumed to be false (the call was successful).
#[serde(rename = "isError")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub is_error: Option<bool>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Content {
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<types::TextAnnotation>,
/// The base64-encoded image data.
#[serde(rename = "data")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub data: Option<String>,
/// The MIME type of the image. Different providers may support different image types.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text content of the message.
#[serde(rename = "text")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub text: Option<String>,
#[serde(rename = "type")]
pub r#type: types::ContentType,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum ContentType {
#[default]
#[serde(rename = "text")]
Text,
#[serde(rename = "image")]
Image,
#[serde(rename = "resource")]
Resource,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ListToolsResult {
/// The list of ToolDescription objects provided by this servlet.
#[serde(rename = "tools")]
pub tools: Vec<types::ToolDescription>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Params {
#[serde(rename = "arguments")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(rename = "name")]
pub name: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum Role {
#[default]
#[serde(rename = "assistant")]
Assistant,
#[serde(rename = "user")]
User,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextAnnotation {
/// Describes who the intended customer of this object or data is.
///
/// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
#[serde(rename = "audience")]
pub audience: Vec<types::Role>,
/// Describes how important this data is for operating the server.
///
/// A value of 1 means "most important," and indicates that the data is
/// effectively required, while 0 means "least important," and indicates that
/// the data is entirely optional.
#[serde(rename = "priority")]
pub priority: f32,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextResourceContents {
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
#[serde(rename = "text")]
pub text: String,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ToolDescription {
/// A description of the tool
#[serde(rename = "description")]
pub description: String,
/// The JSON schema describing the argument input
#[serde(rename = "inputSchema")]
pub input_schema: serde_json::Map<String, serde_json::Value>,
/// The name of the tool. It should match the plugin / binding name.
#[serde(rename = "name")]
pub name: String,
}
}
mod raw_imports {
use super::*;
#[host_fn]
extern "ExtismHost" {}
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/crates-io/src/pdk.rs:
--------------------------------------------------------------------------------
```rust
#![allow(non_snake_case)]
#![allow(unused_macros)]
use extism_pdk::*;
#[allow(unused)]
fn panic_if_key_missing() -> ! {
panic!("missing key");
}
pub(crate) mod internal {
pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
let err = format!("{:?}", e);
let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
unsafe {
extism_pdk::extism::error_set(mem.offset());
}
-1
}
}
#[allow(unused)]
macro_rules! try_input {
() => {{
let x = extism_pdk::input();
match x {
Ok(x) => x,
Err(e) => return internal::return_error(e),
}
}};
}
#[allow(unused)]
macro_rules! try_input_json {
() => {{
let x = extism_pdk::input();
match x {
Ok(extism_pdk::Json(x)) => x,
Err(e) => return internal::return_error(e),
}
}};
}
use base64_serde::base64_serde_type;
base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
mod exports {
use super::*;
#[unsafe(no_mangle)]
pub extern "C" fn call() -> i32 {
let ret =
crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn describe() -> i32 {
let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
}
pub mod types {
use super::*;
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct BlobResourceContents {
/// A base64-encoded string representing the binary data of the item.
#[serde(rename = "blob")]
pub blob: String,
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolRequest {
#[serde(rename = "method")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub method: Option<String>,
#[serde(rename = "params")]
pub params: types::Params,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolResult {
#[serde(rename = "content")]
pub content: Vec<types::Content>,
/// Whether the tool call ended in an error.
///
/// If not set, this is assumed to be false (the call was successful).
#[serde(rename = "isError")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub is_error: Option<bool>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Content {
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<types::TextAnnotation>,
/// The base64-encoded image data.
#[serde(rename = "data")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub data: Option<String>,
/// The MIME type of the image. Different providers may support different image types.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text content of the message.
#[serde(rename = "text")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub text: Option<String>,
#[serde(rename = "type")]
pub r#type: types::ContentType,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum ContentType {
#[default]
#[serde(rename = "text")]
Text,
#[serde(rename = "image")]
Image,
#[serde(rename = "resource")]
Resource,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ListToolsResult {
/// The list of ToolDescription objects provided by this servlet.
#[serde(rename = "tools")]
pub tools: Vec<types::ToolDescription>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Params {
#[serde(rename = "arguments")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(rename = "name")]
pub name: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum Role {
#[default]
#[serde(rename = "assistant")]
Assistant,
#[serde(rename = "user")]
User,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextAnnotation {
/// Describes who the intended customer of this object or data is.
///
/// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
#[serde(rename = "audience")]
pub audience: Vec<types::Role>,
/// Describes how important this data is for operating the server.
///
/// A value of 1 means "most important," and indicates that the data is
/// effectively required, while 0 means "least important," and indicates that
/// the data is entirely optional.
#[serde(rename = "priority")]
pub priority: f32,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextResourceContents {
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
#[serde(rename = "text")]
pub text: String,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ToolDescription {
/// A description of the tool
#[serde(rename = "description")]
pub description: String,
/// The JSON schema describing the argument input
#[serde(rename = "inputSchema")]
pub input_schema: serde_json::Map<String, serde_json::Value>,
/// The name of the tool. It should match the plugin / binding name.
#[serde(rename = "name")]
pub name: String,
}
}
mod raw_imports {
use super::*;
#[host_fn]
extern "ExtismHost" {}
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/eval-py/src/pdk.rs:
--------------------------------------------------------------------------------
```rust
#![allow(non_snake_case)]
#![allow(unused_macros)]
use extism_pdk::*;
#[allow(unused)]
fn panic_if_key_missing() -> ! {
panic!("missing key");
}
pub(crate) mod internal {
pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
let err = format!("{:?}", e);
let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
unsafe {
extism_pdk::extism::error_set(mem.offset());
}
-1
}
}
#[allow(unused)]
macro_rules! try_input {
() => {{
let x = extism_pdk::input();
match x {
Ok(x) => x,
Err(e) => return internal::return_error(e),
}
}};
}
#[allow(unused)]
macro_rules! try_input_json {
() => {{
let x = extism_pdk::input();
match x {
Ok(extism_pdk::Json(x)) => x,
Err(e) => return internal::return_error(e),
}
}};
}
use base64_serde::base64_serde_type;
base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
mod exports {
use super::*;
#[unsafe(no_mangle)]
pub extern "C" fn call() -> i32 {
let ret =
crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn describe() -> i32 {
let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
}
pub mod types {
use super::*;
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct BlobResourceContents {
/// A base64-encoded string representing the binary data of the item.
#[serde(rename = "blob")]
pub blob: String,
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolRequest {
#[serde(rename = "method")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub method: Option<String>,
#[serde(rename = "params")]
pub params: types::Params,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolResult {
#[serde(rename = "content")]
pub content: Vec<types::Content>,
/// Whether the tool call ended in an error.
///
/// If not set, this is assumed to be false (the call was successful).
#[serde(rename = "isError")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub is_error: Option<bool>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Content {
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<types::TextAnnotation>,
/// The base64-encoded image data.
#[serde(rename = "data")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub data: Option<String>,
/// The MIME type of the image. Different providers may support different image types.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text content of the message.
#[serde(rename = "text")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub text: Option<String>,
#[serde(rename = "type")]
pub r#type: types::ContentType,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum ContentType {
#[default]
#[serde(rename = "text")]
Text,
#[serde(rename = "image")]
Image,
#[serde(rename = "resource")]
Resource,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ListToolsResult {
/// The list of ToolDescription objects provided by this servlet.
#[serde(rename = "tools")]
pub tools: Vec<types::ToolDescription>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Params {
#[serde(rename = "arguments")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(rename = "name")]
pub name: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum Role {
#[default]
#[serde(rename = "assistant")]
Assistant,
#[serde(rename = "user")]
User,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextAnnotation {
/// Describes who the intended customer of this object or data is.
///
/// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
#[serde(rename = "audience")]
pub audience: Vec<types::Role>,
/// Describes how important this data is for operating the server.
///
/// A value of 1 means "most important," and indicates that the data is
/// effectively required, while 0 means "least important," and indicates that
/// the data is entirely optional.
#[serde(rename = "priority")]
pub priority: f32,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextResourceContents {
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
#[serde(rename = "text")]
pub text: String,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ToolDescription {
/// A description of the tool
#[serde(rename = "description")]
pub description: String,
/// The JSON schema describing the argument input
#[serde(rename = "inputSchema")]
pub input_schema: serde_json::Map<String, serde_json::Value>,
/// The name of the tool. It should match the plugin / binding name.
#[serde(rename = "name")]
pub name: String,
}
}
mod raw_imports {
use super::*;
#[host_fn]
extern "ExtismHost" {}
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/fetch/src/pdk.rs:
--------------------------------------------------------------------------------
```rust
#![allow(non_snake_case)]
#![allow(unused_macros)]
use extism_pdk::*;
#[allow(unused)]
fn panic_if_key_missing() -> ! {
panic!("missing key");
}
pub(crate) mod internal {
pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
let err = format!("{:?}", e);
let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
unsafe {
extism_pdk::extism::error_set(mem.offset());
}
-1
}
}
#[allow(unused)]
macro_rules! try_input {
() => {{
let x = extism_pdk::input();
match x {
Ok(x) => x,
Err(e) => return internal::return_error(e),
}
}};
}
#[allow(unused)]
macro_rules! try_input_json {
() => {{
let x = extism_pdk::input();
match x {
Ok(extism_pdk::Json(x)) => x,
Err(e) => return internal::return_error(e),
}
}};
}
use base64_serde::base64_serde_type;
base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
mod exports {
use super::*;
#[unsafe(no_mangle)]
pub extern "C" fn call() -> i32 {
let ret =
crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn describe() -> i32 {
let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
}
pub mod types {
use super::*;
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct BlobResourceContents {
/// A base64-encoded string representing the binary data of the item.
#[serde(rename = "blob")]
pub blob: String,
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolRequest {
#[serde(rename = "method")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub method: Option<String>,
#[serde(rename = "params")]
pub params: types::Params,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolResult {
#[serde(rename = "content")]
pub content: Vec<types::Content>,
/// Whether the tool call ended in an error.
///
/// If not set, this is assumed to be false (the call was successful).
#[serde(rename = "isError")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub is_error: Option<bool>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Content {
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<types::TextAnnotation>,
/// The base64-encoded image data.
#[serde(rename = "data")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub data: Option<String>,
/// The MIME type of the image. Different providers may support different image types.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text content of the message.
#[serde(rename = "text")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub text: Option<String>,
#[serde(rename = "type")]
pub r#type: types::ContentType,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum ContentType {
#[default]
#[serde(rename = "text")]
Text,
#[serde(rename = "image")]
Image,
#[serde(rename = "resource")]
Resource,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ListToolsResult {
/// The list of ToolDescription objects provided by this servlet.
#[serde(rename = "tools")]
pub tools: Vec<types::ToolDescription>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Params {
#[serde(rename = "arguments")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(rename = "name")]
pub name: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum Role {
#[default]
#[serde(rename = "assistant")]
Assistant,
#[serde(rename = "user")]
User,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextAnnotation {
/// Describes who the intended customer of this object or data is.
///
/// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
#[serde(rename = "audience")]
pub audience: Vec<types::Role>,
/// Describes how important this data is for operating the server.
///
/// A value of 1 means "most important," and indicates that the data is
/// effectively required, while 0 means "least important," and indicates that
/// the data is entirely optional.
#[serde(rename = "priority")]
pub priority: f32,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextResourceContents {
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
#[serde(rename = "text")]
pub text: String,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ToolDescription {
/// A description of the tool
#[serde(rename = "description")]
pub description: String,
/// The JSON schema describing the argument input
#[serde(rename = "inputSchema")]
pub input_schema: serde_json::Map<String, serde_json::Value>,
/// The name of the tool. It should match the plugin / binding name.
#[serde(rename = "name")]
pub name: String,
}
}
mod raw_imports {
use super::*;
#[host_fn]
extern "ExtismHost" {}
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/fs/src/pdk.rs:
--------------------------------------------------------------------------------
```rust
#![allow(non_snake_case)]
#![allow(unused_macros)]
use extism_pdk::*;
#[allow(unused)]
fn panic_if_key_missing() -> ! {
panic!("missing key");
}
pub(crate) mod internal {
pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
let err = format!("{:?}", e);
let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
unsafe {
extism_pdk::extism::error_set(mem.offset());
}
-1
}
}
#[allow(unused)]
macro_rules! try_input {
() => {{
let x = extism_pdk::input();
match x {
Ok(x) => x,
Err(e) => return internal::return_error(e),
}
}};
}
#[allow(unused)]
macro_rules! try_input_json {
() => {{
let x = extism_pdk::input();
match x {
Ok(extism_pdk::Json(x)) => x,
Err(e) => return internal::return_error(e),
}
}};
}
use base64_serde::base64_serde_type;
base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
mod exports {
use super::*;
#[unsafe(no_mangle)]
pub extern "C" fn call() -> i32 {
let ret =
crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn describe() -> i32 {
let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
}
pub mod types {
use super::*;
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct BlobResourceContents {
/// A base64-encoded string representing the binary data of the item.
#[serde(rename = "blob")]
pub blob: String,
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolRequest {
#[serde(rename = "method")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub method: Option<String>,
#[serde(rename = "params")]
pub params: types::Params,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolResult {
#[serde(rename = "content")]
pub content: Vec<types::Content>,
/// Whether the tool call ended in an error.
///
/// If not set, this is assumed to be false (the call was successful).
#[serde(rename = "isError")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub is_error: Option<bool>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Content {
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<types::TextAnnotation>,
/// The base64-encoded image data.
#[serde(rename = "data")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub data: Option<String>,
/// The MIME type of the image. Different providers may support different image types.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text content of the message.
#[serde(rename = "text")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub text: Option<String>,
#[serde(rename = "type")]
pub r#type: types::ContentType,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum ContentType {
#[default]
#[serde(rename = "text")]
Text,
#[serde(rename = "image")]
Image,
#[serde(rename = "resource")]
Resource,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ListToolsResult {
/// The list of ToolDescription objects provided by this servlet.
#[serde(rename = "tools")]
pub tools: Vec<types::ToolDescription>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Params {
#[serde(rename = "arguments")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(rename = "name")]
pub name: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum Role {
#[default]
#[serde(rename = "assistant")]
Assistant,
#[serde(rename = "user")]
User,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextAnnotation {
/// Describes who the intended customer of this object or data is.
///
/// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
#[serde(rename = "audience")]
pub audience: Vec<types::Role>,
/// Describes how important this data is for operating the server.
///
/// A value of 1 means "most important," and indicates that the data is
/// effectively required, while 0 means "least important," and indicates that
/// the data is entirely optional.
#[serde(rename = "priority")]
pub priority: f32,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextResourceContents {
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
#[serde(rename = "text")]
pub text: String,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ToolDescription {
/// A description of the tool
#[serde(rename = "description")]
pub description: String,
/// The JSON schema describing the argument input
#[serde(rename = "inputSchema")]
pub input_schema: serde_json::Map<String, serde_json::Value>,
/// The name of the tool. It should match the plugin / binding name.
#[serde(rename = "name")]
pub name: String,
}
}
mod raw_imports {
use super::*;
#[host_fn]
extern "ExtismHost" {}
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/gitlab/src/pdk.rs:
--------------------------------------------------------------------------------
```rust
#![allow(non_snake_case)]
#![allow(unused_macros)]
use extism_pdk::*;
#[allow(unused)]
fn panic_if_key_missing() -> ! {
panic!("missing key");
}
pub(crate) mod internal {
pub(crate) fn return_error(e: extism_pdk::Error) -> i32 {
let err = format!("{:?}", e);
let mem = extism_pdk::Memory::from_bytes(&err).unwrap();
unsafe {
extism_pdk::extism::error_set(mem.offset());
}
-1
}
}
#[allow(unused)]
macro_rules! try_input {
() => {{
let x = extism_pdk::input();
match x {
Ok(x) => x,
Err(e) => return internal::return_error(e),
}
}};
}
#[allow(unused)]
macro_rules! try_input_json {
() => {{
let x = extism_pdk::input();
match x {
Ok(extism_pdk::Json(x)) => x,
Err(e) => return internal::return_error(e),
}
}};
}
use base64_serde::base64_serde_type;
base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
mod exports {
use super::*;
#[unsafe(no_mangle)]
pub extern "C" fn call() -> i32 {
let ret =
crate::call(try_input_json!()).and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn describe() -> i32 {
let ret = crate::describe().and_then(|x| extism_pdk::output(extism_pdk::Json(x)));
match ret {
Ok(()) => 0,
Err(e) => internal::return_error(e),
}
}
}
pub mod types {
use super::*;
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct BlobResourceContents {
/// A base64-encoded string representing the binary data of the item.
#[serde(rename = "blob")]
pub blob: String,
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolRequest {
#[serde(rename = "method")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub method: Option<String>,
#[serde(rename = "params")]
pub params: types::Params,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct CallToolResult {
#[serde(rename = "content")]
pub content: Vec<types::Content>,
/// Whether the tool call ended in an error.
///
/// If not set, this is assumed to be false (the call was successful).
#[serde(rename = "isError")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub is_error: Option<bool>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Content {
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<types::TextAnnotation>,
/// The base64-encoded image data.
#[serde(rename = "data")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub data: Option<String>,
/// The MIME type of the image. Different providers may support different image types.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text content of the message.
#[serde(rename = "text")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub text: Option<String>,
#[serde(rename = "type")]
pub r#type: types::ContentType,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum ContentType {
#[default]
#[serde(rename = "text")]
Text,
#[serde(rename = "image")]
Image,
#[serde(rename = "resource")]
Resource,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ListToolsResult {
/// The list of ToolDescription objects provided by this servlet.
#[serde(rename = "tools")]
pub tools: Vec<types::ToolDescription>,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct Params {
#[serde(rename = "arguments")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub arguments: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(rename = "name")]
pub name: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub enum Role {
#[default]
#[serde(rename = "assistant")]
Assistant,
#[serde(rename = "user")]
User,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextAnnotation {
/// Describes who the intended customer of this object or data is.
///
/// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
#[serde(rename = "audience")]
pub audience: Vec<types::Role>,
/// Describes how important this data is for operating the server.
///
/// A value of 1 means "most important," and indicates that the data is
/// effectively required, while 0 means "least important," and indicates that
/// the data is entirely optional.
#[serde(rename = "priority")]
pub priority: f32,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct TextResourceContents {
/// The MIME type of this resource, if known.
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
#[serde(rename = "text")]
pub text: String,
/// The URI of this resource.
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(
Default,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
extism_pdk::FromBytes,
extism_pdk::ToBytes,
)]
#[encoding(Json)]
pub struct ToolDescription {
/// A description of the tool
#[serde(rename = "description")]
pub description: String,
/// The JSON schema describing the argument input
#[serde(rename = "inputSchema")]
pub input_schema: serde_json::Map<String, serde_json::Value>,
/// The name of the tool. It should match the plugin / binding name.
#[serde(rename = "name")]
pub name: String,
}
}
mod raw_imports {
use super::*;
#[host_fn]
extern "ExtismHost" {}
}
```