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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/examples/plugins/v1/memory/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM rust:1.88-slim AS builder
 2 | 
 3 | RUN rustup target add wasm32-wasip1 && \
 4 |     rustup component add rust-std --target wasm32-wasip1 && \
 5 |     cargo install cargo-auditable
 6 | 
 7 | # Install wasi-sdk
 8 | ENV WASI_OS=linux \
 9 |     WASI_VERSION=25 \
10 |     WASI_VERSION_FULL=25.0
11 | 
12 | # Detect architecture and set WASI_ARCH accordingly
13 | RUN apt-get update && apt-get install -y wget && \
14 |     ARCH=$(uname -m) && \
15 |     if [ "$ARCH" = "x86_64" ]; then \
16 |         export WASI_ARCH=x86_64; \
17 |     elif [ "$ARCH" = "aarch64" ]; then \
18 |         export WASI_ARCH=arm64; \
19 |     else \
20 |         echo "Unsupported architecture: $ARCH" && exit 1; \
21 |     fi && \
22 |     cd /opt && \
23 |     wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_VERSION}/wasi-sdk-${WASI_VERSION_FULL}-${WASI_ARCH}-${WASI_OS}.tar.gz && \
24 |     tar xvf wasi-sdk-${WASI_VERSION_FULL}-${WASI_ARCH}-${WASI_OS}.tar.gz && \
25 |     rm wasi-sdk-${WASI_VERSION_FULL}-${WASI_ARCH}-${WASI_OS}.tar.gz && \
26 |     mv wasi-sdk-${WASI_VERSION_FULL}-${WASI_ARCH}-${WASI_OS} wasi-sdk
27 | 
28 | WORKDIR /workspace
29 | COPY . .
30 | RUN cargo fetch
31 | ENV WASI_SDK_PATH=/opt/wasi-sdk
32 | ENV CC_wasm32_wasip1="${WASI_SDK_PATH}/bin/clang --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot"
33 | 
34 | RUN cargo auditable build --release --target wasm32-wasip1
35 | 
36 | FROM scratch
37 | WORKDIR /
38 | COPY --from=builder /workspace/target/wasm32-wasip1/release/plugin.wasm /plugin.wasm
39 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/sqlite/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM rust:1.88-slim AS builder
 2 | 
 3 | RUN rustup target add wasm32-wasip1 && \
 4 |     rustup component add rust-std --target wasm32-wasip1 && \
 5 |     cargo install cargo-auditable
 6 | 
 7 | # Install wasi-sdk
 8 | ENV WASI_OS=linux \
 9 |     WASI_VERSION=25 \
10 |     WASI_VERSION_FULL=25.0
11 | 
12 | # Detect architecture and set WASI_ARCH accordingly
13 | RUN apt-get update && apt-get install -y wget && \
14 |     ARCH=$(uname -m) && \
15 |     if [ "$ARCH" = "x86_64" ]; then \
16 |         export WASI_ARCH=x86_64; \
17 |     elif [ "$ARCH" = "aarch64" ]; then \
18 |         export WASI_ARCH=arm64; \
19 |     else \
20 |         echo "Unsupported architecture: $ARCH" && exit 1; \
21 |     fi && \
22 |     cd /opt && \
23 |     wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_VERSION}/wasi-sdk-${WASI_VERSION_FULL}-${WASI_ARCH}-${WASI_OS}.tar.gz && \
24 |     tar xvf wasi-sdk-${WASI_VERSION_FULL}-${WASI_ARCH}-${WASI_OS}.tar.gz && \
25 |     rm wasi-sdk-${WASI_VERSION_FULL}-${WASI_ARCH}-${WASI_OS}.tar.gz && \
26 |     mv wasi-sdk-${WASI_VERSION_FULL}-${WASI_ARCH}-${WASI_OS} wasi-sdk
27 | 
28 | WORKDIR /workspace
29 | COPY . .
30 | RUN cargo fetch
31 | ENV WASI_SDK_PATH=/opt/wasi-sdk
32 | ENV CC_wasm32_wasip1="${WASI_SDK_PATH}/bin/clang --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot"
33 | 
34 | RUN cargo auditable build --release --target wasm32-wasip1
35 | 
36 | FROM scratch
37 | WORKDIR /
38 | COPY --from=builder /workspace/target/wasm32-wasip1/release/plugin.wasm /plugin.wasm
39 | 
```

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

```rust
 1 | mod pdk;
 2 | 
 3 | use extism_pdk::*;
 4 | use pdk::types::*;
 5 | use serde_json::json;
 6 | 
 7 | pub(crate) fn call(_input: CallToolRequest) -> Result<CallToolResult, Error> {
 8 |     let request = HttpRequest::new("https://1.1.1.1/cdn-cgi/trace");
 9 |     let response = http::request::<Vec<u8>>(&request, None)
10 |         .map_err(|e| Error::msg(format!("Failed to make HTTP request: {}", e)))?;
11 | 
12 |     let text = String::from_utf8(response.body().to_vec())
13 |         .map_err(|e| Error::msg(format!("Failed to parse response as UTF-8: {}", e)))?;
14 | 
15 |     // Parse the response to extract IP address
16 |     let ip = text
17 |         .lines()
18 |         .find(|line| line.starts_with("ip="))
19 |         .map(|line| line.trim_start_matches("ip="))
20 |         .ok_or_else(|| Error::msg("Could not find IP address in response"))?;
21 | 
22 |     Ok(CallToolResult {
23 |         is_error: None,
24 |         content: vec![Content {
25 |             annotations: None,
26 |             text: Some(ip.to_string()),
27 |             mime_type: Some("text/plain".into()),
28 |             r#type: ContentType::Text,
29 |             data: None,
30 |         }],
31 |     })
32 | }
33 | 
34 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
35 |     Ok(ListToolsResult {
36 |         tools: vec![ToolDescription {
37 |             name: "myip".into(),
38 |             description: "Get the current IP address using Cloudflare's service".into(),
39 |             input_schema: json!({
40 |                 "type": "object",
41 |                 "properties": {},
42 |                 "required": [],
43 |             })
44 |             .as_object()
45 |             .unwrap()
46 |             .clone(),
47 |         }],
48 |     })
49 | }
50 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/crypto-price/main.go:
--------------------------------------------------------------------------------

```go
 1 | package main
 2 | 
 3 | import (
 4 | 	"encoding/json"
 5 | 	"errors"
 6 | 	"fmt"
 7 | 	"strings"
 8 | 
 9 | 	pdk "github.com/extism/go-pdk"
10 | )
11 | 
12 | func Call(input CallToolRequest) (CallToolResult, error) {
13 | 	args := input.Params.Arguments
14 | 	if args == nil {
15 | 		return CallToolResult{}, errors.New("Arguments must be provided")
16 | 	}
17 | 
18 | 	argsMap := args.(map[string]interface{})
19 | 	fmt.Println("argsMap", argsMap)
20 | 	return getCryptoPrice(argsMap)
21 | }
22 | 
23 | func getCryptoPrice(args map[string]interface{}) (CallToolResult, error) {
24 | 	symbol, ok := args["symbol"].(string)
25 | 	if !ok {
26 | 		return CallToolResult{}, errors.New("symbol must be provided")
27 | 	}
28 | 
29 | 	// Convert symbol to uppercase
30 | 	symbol = strings.ToUpper(symbol)
31 | 
32 | 	// Use CoinGecko API to get the price
33 | 	url := fmt.Sprintf("https://api.coingecko.com/api/v3/simple/price?ids=%s&vs_currencies=usd", strings.ToLower(symbol))
34 | 	req := pdk.NewHTTPRequest(pdk.MethodGet, url)
35 | 	resp := req.Send()
36 | 
37 | 	var result map[string]map[string]float64
38 | 	if err := json.Unmarshal(resp.Body(), &result); err != nil {
39 | 		return CallToolResult{}, fmt.Errorf("failed to parse response: %v", err)
40 | 	}
41 | 
42 | 	if price, ok := result[strings.ToLower(symbol)]["usd"]; ok {
43 | 		priceStr := fmt.Sprintf("%.2f USD", price)
44 | 		return CallToolResult{
45 | 			Content: []Content{
46 | 				{
47 | 					Type: ContentTypeText,
48 | 					Text: &priceStr,
49 | 				},
50 | 			},
51 | 		}, nil
52 | 	}
53 | 
54 | 	return CallToolResult{}, fmt.Errorf("price not found for %s", symbol)
55 | }
56 | 
57 | func Describe() (ListToolsResult, error) {
58 | 	return ListToolsResult{
59 | 		Tools: []ToolDescription{
60 | 			{
61 | 				Name:        "crypto-price",
62 | 				Description: "Get the current price of a cryptocurrency in USD",
63 | 				InputSchema: map[string]interface{}{
64 | 					"type":     "object",
65 | 					"required": []string{"symbol"},
66 | 					"properties": map[string]interface{}{
67 | 						"symbol": map[string]interface{}{
68 | 							"type":        "string",
69 | 							"description": "the cryptocurrency symbol/id (e.g., bitcoin, ethereum)",
70 | 						},
71 | 					},
72 | 				},
73 | 			},
74 | 		},
75 | 	}, nil
76 | }
77 | 
```

--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------

```toml
 1 | [package]
 2 | name = "hyper-mcp"
 3 | version = "0.1.8"
 4 | edition = "2024"
 5 | authors = ["Tuan Anh Tran <[email protected]>"]
 6 | description = " A fast, secure MCP server that extends its capabilities through WebAssembly plugins"
 7 | keywords = ["rust", "ai", "mcp", "cli"]
 8 | categories = ["command-line-utilities"]
 9 | readme = "README.md"
10 | license = "Apache-2.0"
11 | repository = "https://github.com/tuananh/hyper-mcp"
12 | homepage = "https://github.com/tuananh/hyper-mcp"
13 | documentation = "https://github.com/tuananh/hyper-mcp"
14 | 
15 | [dependencies]
16 | anyhow = "1.0.98"
17 | async-trait = "0.1"
18 | aws-config = { version = "1.8.2", features = ["behavior-version-latest"] }
19 | aws-sdk-s3 = "1.98.0"
20 | axum = "0.8.4"
21 | bytesize = "2.0.1"
22 | clap = { version = "4.5.40", features = ["derive", "env"] }
23 | ctor = "0.6"
24 | dashmap = "6.1.0"
25 | dirs = "6.0.0"
26 | docker_credential = "1.3.2"
27 | extism = "1.12.0"
28 | extism-convert = "1.12.0"
29 | flate2 = "1.1.2"
30 | hex = "0.4.3"
31 | keyring = { version = "3.6.3", features = [
32 |     "apple-native",
33 |     "linux-native",
34 |     "windows-native",
35 | ] }
36 | oci-client = "0.15.0"
37 | once_cell = "1.21.3"
38 | rmcp = { version = "0.9.0", features = [
39 |     "elicitation",
40 |     "server",
41 |     "transport-io",
42 |     "transport-sse-server",
43 |     "transport-streamable-http-server",
44 | ] }
45 | regex = { version = "1.11.3", features = ["unicode", "perf"] }
46 | reqwest = { version = "0.12.21", features = ["json"] }
47 | serde = { version = "1.0.219", features = ["derive"] }
48 | serde_json = "1.0.140"
49 | serde_yaml = "0.9.34"
50 | serde_with = "3.15"
51 | sha2 = "0.10.9"
52 | sigstore = { version = "0.13.0", features = ["cosign", "verify", "bundle"] }
53 | tar = "0.4.44"
54 | tokio = { version = "1.45.1", features = ["full"] }
55 | tokio-util = "0.7"
56 | toml = "0.9.0"
57 | tracing = "0.1.41"
58 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
59 | url = { version = "2", features = ["serde"] }
60 | uuid = { version = "1.18", features = ["serde"] }
61 | 
62 | [dev-dependencies]
63 | futures = "0.3.31"
64 | rmcp = { version = "0.9.0", features = [
65 |     "client",
66 |     "transport-async-rw",
67 | ] }
68 | tempfile = "3.12.0"
69 | tokio-test = "0.4.4"
70 | tokio-util = "0.7.16"
71 | 
72 | [[bin]]
73 | name = "hyper-mcp"
74 | path = "src/main.rs"
75 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/meme-generator/generate_embedded.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | import json
 3 | 
 4 | def main():
 5 |     # Read templates.json to get list of template IDs
 6 |     with open('templates.json', 'r') as f:
 7 |         templates = json.load(f)
 8 | 
 9 |     template_ids = [t['id'] for t in templates]
10 | 
11 |     # Start generating embedded.rs
12 |     output = []
13 |     output.append('// Embed templates.json')
14 |     output.append('pub const TEMPLATES_JSON: &str = include_str!("../templates.json");')
15 |     output.append('')
16 |     output.append('// Embed font data')
17 |     output.append('pub const FONT_DATA: &[u8] = include_bytes!("../assets/fonts/TitilliumWeb-Black.ttf");')
18 |     output.append('')
19 |     output.append('// Function to get template config')
20 |     output.append('pub fn get_template_config(template_id: &str) -> Option<&\'static str> {')
21 |     output.append('    match template_id {')
22 | 
23 |     # Add template configs
24 |     for template_id in template_ids:
25 |         config_path = f'assets/templates/{template_id}/config.yml'
26 |         if os.path.exists(config_path):
27 |             output.append(f'        "{template_id}" => Some(include_str!("../assets/templates/{template_id}/config.yml")),')
28 | 
29 |     output.append('        _ => None')
30 |     output.append('    }')
31 |     output.append('}')
32 |     output.append('')
33 | 
34 |     # Add template images
35 |     output.append('// Function to get template image')
36 |     output.append('pub fn get_template_image(template_id: &str, image_name: &str) -> Option<&\'static [u8]> {')
37 |     output.append('    match (template_id, image_name) {')
38 | 
39 |     for template_id in template_ids:
40 |         template_dir = f'assets/templates/{template_id}'
41 |         if os.path.exists(template_dir):
42 |             for file in os.listdir(template_dir):
43 |                 if file.endswith(('.jpg', '.png', '.gif')):
44 |                     output.append(f'        ("{template_id}", "{file}") => Some(include_bytes!("../assets/templates/{template_id}/{file}")),')
45 | 
46 |     output.append('        _ => None')
47 |     output.append('    }')
48 |     output.append('}')
49 | 
50 |     # Write output
51 |     with open('src/embedded.rs', 'w') as f:
52 |         f.write('\n'.join(output))
53 | 
54 | if __name__ == '__main__':
55 |     main()
56 | 
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [main]
 6 |   pull_request:
 7 |     branches: [main]
 8 |   workflow_dispatch:
 9 | 
10 | env:
11 |   CARGO_TERM_COLOR: always
12 | 
13 | jobs:
14 |   build:
15 |     name: Build
16 |     runs-on: ubuntu-latest
17 | 
18 |     steps:
19 |       - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
20 |         with:
21 |           fetch-depth: 0
22 |           submodules: true
23 | 
24 |       - name: Install Rust toolchain
25 |         uses: dtolnay/rust-toolchain@stable
26 |         with:
27 |           components: rustfmt, clippy
28 | 
29 |       - name: Install WASM target
30 |         run: rustup target add wasm32-wasip1
31 | 
32 |       - name: Cache dependencies
33 |         uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
34 |         with:
35 |           workspaces: "., examples/plugins/v1/*"
36 | 
37 |       - name: Run clippy
38 |         run: cargo clippy -- -D warnings
39 | 
40 |       - name: Check formatting
41 |         run: cargo fmt -- --check
42 | 
43 |       - name: Run tests
44 |         run: cargo test --workspace --all-features
45 | 
46 |       - name: Build hyper-mcp
47 |         run: cargo build
48 | 
49 |       - name: Set up TinyGo
50 |         uses: acifani/setup-tinygo@db56321a62b9a67922bb9ac8f9d085e218807bb3 # v2.0.1
51 |         with:
52 |           tinygo-version: "0.37.0"
53 | 
54 |       - name: Build example plugins
55 |         run: |
56 |           for plugin in examples/plugins/v{1,2}/*/; do
57 |             plugin_name=$(basename $plugin)
58 |             echo "Building plugin: $plugin"
59 |             current_dir="$PWD"
60 |             cd $plugin
61 |             case "$plugin_name" in
62 |               "crypto-price"|"github")
63 |                 # --- Go-based plugins ---
64 |                 GOOS=wasip1 GOARCH=wasm tinygo build -no-debug -panic=trap -scheduler=none -o plugin.wasm
65 |                 ;;
66 |               "arxiv"|"context7"|"crates-io"|"crypto-price"|"eval-py"|"fetch"|"fs"|"gitlab"|"gomodule"|"hash"|"maven"|"meme-generator"|"myip"|"qdrant"|"qr-code"|"rstime"|"serper"|"think"|"time"|"tool-list-changed")
67 |                 # --- Rust-based plugins ---
68 |                 cargo build --release --target wasm32-wasip1
69 |                 ;;
70 |               *)
71 |                 echo "Skipping: $plugin_name"
72 |             esac
73 |             cd $current_dir
74 |           done
75 | 
```

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

```rust
 1 | mod pdk;
 2 | 
 3 | use base64::Engine;
 4 | use extism_pdk::*;
 5 | use pdk::types::*;
 6 | use qrcode_png::{Color, QrCode, QrCodeEcc};
 7 | use serde_json::{Map, Value, json};
 8 | 
 9 | pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
10 |     extism_pdk::log!(
11 |         LogLevel::Info,
12 |         "called with args: {:?}",
13 |         input.params.arguments
14 |     );
15 |     let args = input.params.arguments.unwrap_or_default();
16 |     let ecc = to_ecc(
17 |         args.get("ecc")
18 |             .cloned()
19 |             .unwrap_or_else(|| json!(4))
20 |             .as_number()
21 |             .unwrap()
22 |             .is_u64() as u8,
23 |     );
24 | 
25 |     let data = match args.get("data") {
26 |         Some(v) => v.as_str().unwrap(),
27 |         None => return Err(Error::msg("`data` must be available")),
28 |     };
29 | 
30 |     let mut code = QrCode::new(data, ecc)?;
31 |     code.margin(10);
32 |     code.zoom(10);
33 | 
34 |     let b = code.generate(Color::Grayscale(0, 255))?;
35 |     let data = base64::engine::general_purpose::STANDARD.encode(b);
36 | 
37 |     Ok(CallToolResult {
38 |         is_error: None,
39 |         content: vec![Content {
40 |             annotations: None,
41 |             text: None,
42 |             mime_type: Some("image/png".into()),
43 |             r#type: ContentType::Image,
44 |             data: Some(data),
45 |         }],
46 |     })
47 | }
48 | 
49 | fn to_ecc(num: u8) -> QrCodeEcc {
50 |     if num < 4 {
51 |         return unsafe { std::mem::transmute::<u8, QrCodeEcc>(num) };
52 |     }
53 | 
54 |     QrCodeEcc::High
55 | }
56 | 
57 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
58 |     Ok(ListToolsResult {
59 |         tools: vec![ToolDescription {
60 |             name: "qr-code".into(),
61 |             description: "Convert data like a message or URL to a QR code (resulting in a PNG file)".into(),
62 |             input_schema: json!({
63 |                 "type": "object",
64 |                 "properties": {
65 |                     "data": {
66 |                         "type": "string",
67 |                         "description": "data to convert to a QR code PNG"
68 |                     },
69 |                     "ecc": {
70 |                         "type": "number",
71 |                         "description": "Error correction level (range from 1 [low] to 4 [high], default to 4 unless user specifies)"
72 |                     }
73 |                 },
74 |                 "required": ["data"]
75 |             }).as_object().unwrap().clone(),
76 |         }],
77 |     })
78 | }
79 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v2/rstime/src/pdk/exports.rs:
--------------------------------------------------------------------------------

```rust
  1 | use extism_pdk::{Error, Json, Memory, extism::error_set, input, output};
  2 | 
  3 | pub(crate) fn return_error(e: Error) -> i32 {
  4 |     let err = format!("{:?}", e);
  5 |     let mem = Memory::from_bytes(&err).unwrap();
  6 |     unsafe {
  7 |         error_set(mem.offset());
  8 |     }
  9 |     -1
 10 | }
 11 | 
 12 | macro_rules! try_input_json {
 13 |     () => {{
 14 |         let x = input();
 15 |         match x {
 16 |             Ok(Json(x)) => x,
 17 |             Err(e) => return return_error(e),
 18 |         }
 19 |     }};
 20 | }
 21 | 
 22 | #[no_mangle]
 23 | pub extern "C" fn call_tool() -> i32 {
 24 |     let ret = crate::call_tool(try_input_json!()).and_then(|x| output(Json(x)));
 25 | 
 26 |     match ret {
 27 |         Ok(()) => 0,
 28 |         Err(e) => return_error(e),
 29 |     }
 30 | }
 31 | 
 32 | #[no_mangle]
 33 | pub extern "C" fn complete() -> i32 {
 34 |     let ret = crate::complete(try_input_json!()).and_then(|x| output(Json(x)));
 35 | 
 36 |     match ret {
 37 |         Ok(()) => 0,
 38 |         Err(e) => return_error(e),
 39 |     }
 40 | }
 41 | 
 42 | #[no_mangle]
 43 | pub extern "C" fn get_prompt() -> i32 {
 44 |     let ret = crate::get_prompt(try_input_json!()).and_then(|x| output(Json(x)));
 45 | 
 46 |     match ret {
 47 |         Ok(()) => 0,
 48 |         Err(e) => return_error(e),
 49 |     }
 50 | }
 51 | 
 52 | #[no_mangle]
 53 | pub extern "C" fn list_prompts() -> i32 {
 54 |     let ret = crate::list_prompts(try_input_json!()).and_then(|x| output(Json(x)));
 55 | 
 56 |     match ret {
 57 |         Ok(()) => 0,
 58 |         Err(e) => return_error(e),
 59 |     }
 60 | }
 61 | 
 62 | #[no_mangle]
 63 | pub extern "C" fn list_resource_templates() -> i32 {
 64 |     let ret = crate::list_resource_templates(try_input_json!()).and_then(|x| output(Json(x)));
 65 | 
 66 |     match ret {
 67 |         Ok(()) => 0,
 68 |         Err(e) => return_error(e),
 69 |     }
 70 | }
 71 | 
 72 | #[no_mangle]
 73 | pub extern "C" fn list_resources() -> i32 {
 74 |     let ret = crate::list_resources(try_input_json!()).and_then(|x| output(Json(x)));
 75 | 
 76 |     match ret {
 77 |         Ok(()) => 0,
 78 |         Err(e) => return_error(e),
 79 |     }
 80 | }
 81 | 
 82 | #[no_mangle]
 83 | pub extern "C" fn list_tools() -> i32 {
 84 |     let ret = crate::list_tools(try_input_json!()).and_then(|x| output(Json(x)));
 85 | 
 86 |     match ret {
 87 |         Ok(()) => 0,
 88 |         Err(e) => return_error(e),
 89 |     }
 90 | }
 91 | 
 92 | #[no_mangle]
 93 | pub extern "C" fn on_roots_list_changed() -> i32 {
 94 |     let ret = crate::on_roots_list_changed(try_input_json!()).and_then(output);
 95 | 
 96 |     match ret {
 97 |         Ok(()) => 0,
 98 |         Err(e) => return_error(e),
 99 |     }
100 | }
101 | 
102 | #[no_mangle]
103 | pub extern "C" fn read_resource() -> i32 {
104 |     let ret = crate::read_resource(try_input_json!()).and_then(|x| output(Json(x)));
105 | 
106 |     match ret {
107 |         Ok(()) => 0,
108 |         Err(e) => return_error(e),
109 |     }
110 | }
111 | 
```

--------------------------------------------------------------------------------
/templates/plugins/rust/src/pdk/exports.rs:
--------------------------------------------------------------------------------

```rust
  1 | use extism_pdk::{Error, Json, Memory, extism::error_set, input, output};
  2 | 
  3 | pub(crate) fn return_error(e: Error) -> i32 {
  4 |     let err = format!("{:?}", e);
  5 |     let mem = Memory::from_bytes(&err).unwrap();
  6 |     unsafe {
  7 |         error_set(mem.offset());
  8 |     }
  9 |     -1
 10 | }
 11 | 
 12 | macro_rules! try_input_json {
 13 |     () => {{
 14 |         let x = input();
 15 |         match x {
 16 |             Ok(Json(x)) => x,
 17 |             Err(e) => return return_error(e),
 18 |         }
 19 |     }};
 20 | }
 21 | 
 22 | #[no_mangle]
 23 | pub extern "C" fn call_tool() -> i32 {
 24 |     let ret = crate::call_tool(try_input_json!()).and_then(|x| output(Json(x)));
 25 | 
 26 |     match ret {
 27 |         Ok(()) => 0,
 28 |         Err(e) => return_error(e),
 29 |     }
 30 | }
 31 | 
 32 | #[no_mangle]
 33 | pub extern "C" fn complete() -> i32 {
 34 |     let ret = crate::complete(try_input_json!()).and_then(|x| output(Json(x)));
 35 | 
 36 |     match ret {
 37 |         Ok(()) => 0,
 38 |         Err(e) => return_error(e),
 39 |     }
 40 | }
 41 | 
 42 | #[no_mangle]
 43 | pub extern "C" fn get_prompt() -> i32 {
 44 |     let ret = crate::get_prompt(try_input_json!()).and_then(|x| output(Json(x)));
 45 | 
 46 |     match ret {
 47 |         Ok(()) => 0,
 48 |         Err(e) => return_error(e),
 49 |     }
 50 | }
 51 | 
 52 | #[no_mangle]
 53 | pub extern "C" fn list_prompts() -> i32 {
 54 |     let ret = crate::list_prompts(try_input_json!()).and_then(|x| output(Json(x)));
 55 | 
 56 |     match ret {
 57 |         Ok(()) => 0,
 58 |         Err(e) => return_error(e),
 59 |     }
 60 | }
 61 | 
 62 | #[no_mangle]
 63 | pub extern "C" fn list_resource_templates() -> i32 {
 64 |     let ret = crate::list_resource_templates(try_input_json!()).and_then(|x| output(Json(x)));
 65 | 
 66 |     match ret {
 67 |         Ok(()) => 0,
 68 |         Err(e) => return_error(e),
 69 |     }
 70 | }
 71 | 
 72 | #[no_mangle]
 73 | pub extern "C" fn list_resources() -> i32 {
 74 |     let ret = crate::list_resources(try_input_json!()).and_then(|x| output(Json(x)));
 75 | 
 76 |     match ret {
 77 |         Ok(()) => 0,
 78 |         Err(e) => return_error(e),
 79 |     }
 80 | }
 81 | 
 82 | #[no_mangle]
 83 | pub extern "C" fn list_tools() -> i32 {
 84 |     let ret = crate::list_tools(try_input_json!()).and_then(|x| output(Json(x)));
 85 | 
 86 |     match ret {
 87 |         Ok(()) => 0,
 88 |         Err(e) => return_error(e),
 89 |     }
 90 | }
 91 | 
 92 | #[no_mangle]
 93 | pub extern "C" fn on_roots_list_changed() -> i32 {
 94 |     let ret = crate::on_roots_list_changed(try_input_json!()).and_then(output);
 95 | 
 96 |     match ret {
 97 |         Ok(()) => 0,
 98 |         Err(e) => return_error(e),
 99 |     }
100 | }
101 | 
102 | #[no_mangle]
103 | pub extern "C" fn read_resource() -> i32 {
104 |     let ret = crate::read_resource(try_input_json!()).and_then(|x| output(Json(x)));
105 | 
106 |     match ret {
107 |         Ok(()) => 0,
108 |         Err(e) => return_error(e),
109 |     }
110 | }
111 | 
```

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

```rust
 1 | mod pdk;
 2 | 
 3 | use extism_pdk::*;
 4 | use json::Value;
 5 | use pdk::types::{
 6 |     CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
 7 | };
 8 | use serde_json::json;
 9 | 
10 | pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
11 |     match input.params.name.as_str() {
12 |         "think" => think(input),
13 |         _ => Ok(CallToolResult {
14 |             is_error: Some(true),
15 |             content: vec![Content {
16 |                 annotations: None,
17 |                 text: Some(format!("Unknown tool: {}", input.params.name)),
18 |                 mime_type: None,
19 |                 r#type: ContentType::Text,
20 |                 data: None,
21 |             }],
22 |         }),
23 |     }
24 | }
25 | 
26 | fn think(input: CallToolRequest) -> Result<CallToolResult, Error> {
27 |     let args = input.params.arguments.unwrap_or_default();
28 |     if let Some(Value::String(thought)) = args.get("thought") {
29 |         Ok(CallToolResult {
30 |             is_error: None,
31 |             content: vec![Content {
32 |                 annotations: None,
33 |                 text: Some(thought.clone()),
34 |                 mime_type: Some("text/plain".to_string()),
35 |                 r#type: ContentType::Text,
36 |                 data: None,
37 |             }],
38 |         })
39 |     } else {
40 |         Ok(CallToolResult {
41 |             is_error: Some(true),
42 |             content: vec![Content {
43 |                 annotations: None,
44 |                 text: Some("Please provide a 'thought' string.".into()),
45 |                 mime_type: None,
46 |                 r#type: ContentType::Text,
47 |                 data: None,
48 |             }],
49 |         })
50 |     }
51 | }
52 | 
53 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
54 |     Ok(ListToolsResult{
55 |         tools: vec![
56 |             ToolDescription {
57 |                 name: "think".into(),
58 |                 description: "Use the tool to think about something. It will not obtain new information or change the database, but just append the thought to the log. Use it when complex reasoning or some cache memory is needed.".into(),
59 |                 input_schema: json!({
60 |                     "type": "object",
61 |                     "properties": {
62 |                         "thought": {
63 |                             "type": "string",
64 |                             "description": "A thought to think about.",
65 |                         },
66 |                     },
67 |                     "required": ["thought"],
68 |                 })
69 |                 .as_object()
70 |                 .unwrap()
71 |                 .clone(),
72 |             },
73 |         ],
74 |     })
75 | }
76 | 
```

--------------------------------------------------------------------------------
/src/cli.rs:
--------------------------------------------------------------------------------

```rust
 1 | use clap::Parser;
 2 | use std::path::PathBuf;
 3 | 
 4 | pub const DEFAULT_BIND_ADDRESS: &str = "127.0.0.1:3001";
 5 | 
 6 | #[derive(Parser, Clone)]
 7 | #[command(author = "Tuan Anh Tran <[email protected]>", version = env!("CARGO_PKG_VERSION"), about, long_about = None)]
 8 | pub struct Cli {
 9 |     #[arg(short, long, value_name = "FILE")]
10 |     pub config_file: Option<PathBuf>,
11 | 
12 |     #[arg(
13 |         long = "transport",
14 |         value_name = "TRANSPORT",
15 |         env = "HYPER_MCP_TRANSPORT",
16 |         default_value = "stdio",
17 |         value_parser = ["stdio", "sse", "streamable-http"]
18 |     )]
19 |     pub transport: String,
20 | 
21 |     #[arg(
22 |         long = "bind-address",
23 |         value_name = "ADDRESS",
24 |         env = "HYPER_MCP_BIND_ADDRESS",
25 |         default_value = DEFAULT_BIND_ADDRESS
26 |     )]
27 |     pub bind_address: String,
28 | 
29 |     #[arg(
30 |         long = "insecure-skip-signature",
31 |         help = "Skip OCI image signature verification. Will override the value in your config file if set.",
32 |         env = "HYPER_MCP_INSECURE_SKIP_SIGNATURE"
33 |     )]
34 |     pub insecure_skip_signature: Option<bool>,
35 | 
36 |     #[arg(
37 |         long = "use-sigstore-tuf-data",
38 |         help = "Use Sigstore TUF data for OCI verification. Will override the value in your config file if set.",
39 |         env = "HYPER_MCP_USE_SIGSTORE_TUF_DATA"
40 |     )]
41 |     pub use_sigstore_tuf_data: Option<bool>,
42 | 
43 |     #[arg(
44 |         long = "rekor-pub-keys",
45 |         help = "Path to Rekor public keys for OCI verification. Will override the value in your config file if set.",
46 |         env = "HYPER_MCP_REKOR_PUB_KEYS"
47 |     )]
48 |     pub rekor_pub_keys: Option<PathBuf>,
49 | 
50 |     #[arg(
51 |         long = "fulcio-certs",
52 |         help = "Path to Fulcio certificates for OCI verification. Will override the value in your config file if set.",
53 |         env = "HYPER_MCP_FULCIO_CERTS"
54 |     )]
55 |     pub fulcio_certs: Option<PathBuf>,
56 | 
57 |     #[arg(
58 |         long = "cert-issuer",
59 |         help = "Certificate issuer to verify OCI against. Will override the value in your config file if set.",
60 |         env = "HYPER_MCP_CERT_ISSUER"
61 |     )]
62 |     pub cert_issuer: Option<String>,
63 | 
64 |     #[arg(
65 |         long = "cert-email",
66 |         help = "Certificate email to verify OCI against. Will override the value in your config file if set.",
67 |         env = "HYPER_MCP_CERT_EMAIL"
68 |     )]
69 |     pub cert_email: Option<String>,
70 | 
71 |     #[arg(
72 |         long = "cert-url",
73 |         help = "Certificate URL to verify OCI against. Will override the value in your config file if set.",
74 |         env = "HYPER_MCP_CERT_URL"
75 |     )]
76 |     pub cert_url: Option<String>,
77 | }
78 | 
79 | impl Default for Cli {
80 |     fn default() -> Self {
81 |         Self {
82 |             config_file: None,
83 |             transport: "stdio".to_string(),
84 |             bind_address: DEFAULT_BIND_ADDRESS.to_string(),
85 |             insecure_skip_signature: None,
86 |             use_sigstore_tuf_data: None,
87 |             rekor_pub_keys: None,
88 |             fulcio_certs: None,
89 |             cert_issuer: None,
90 |             cert_email: None,
91 |             cert_url: None,
92 |         }
93 |     }
94 | }
95 | 
```

--------------------------------------------------------------------------------
/templates/plugins/rust/src/lib.rs:
--------------------------------------------------------------------------------

```rust
 1 | mod pdk;
 2 | 
 3 | use anyhow::{Result, anyhow};
 4 | use pdk::types::*;
 5 | 
 6 | pub(crate) fn call_tool(_input: CallToolRequest) -> Result<CallToolResult> {
 7 |     Err(anyhow!("call_tool not implemented"))
 8 | }
 9 | 
10 | // Provide completion suggestions for a partially-typed input.
11 | //
12 | // 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.
13 | pub(crate) fn complete(_input: CompleteRequest) -> Result<CompleteResult> {
14 |     Ok(CompleteResult::default())
15 | }
16 | 
17 | // Retrieve a specific prompt by name.
18 | //
19 | // This function is called when the user requests a specific prompt. The plugin should return the prompt details including messages and optional description.
20 | pub(crate) fn get_prompt(_input: GetPromptRequest) -> Result<GetPromptResult> {
21 |     Err(anyhow!("get_prompt not implemented"))
22 | }
23 | 
24 | // List all available prompts.
25 | //
26 | // 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.
27 | pub(crate) fn list_prompts(_input: ListPromptsRequest) -> Result<ListPromptsResult> {
28 |     Ok(ListPromptsResult::default())
29 | }
30 | 
31 | // List all available resource templates.
32 | //
33 | // 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.
34 | pub(crate) fn list_resource_templates(
35 |     _input: ListResourceTemplatesRequest,
36 | ) -> Result<ListResourceTemplatesResult> {
37 |     Ok(ListResourceTemplatesResult::default())
38 | }
39 | 
40 | // List all available resources.
41 | //
42 | // 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.
43 | pub(crate) fn list_resources(_input: ListResourcesRequest) -> Result<ListResourcesResult> {
44 |     Ok(ListResourcesResult::default())
45 | }
46 | 
47 | // List all available tools.
48 | //
49 | // 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.
50 | pub(crate) fn list_tools(_input: ListToolsRequest) -> Result<ListToolsResult> {
51 |     Ok(ListToolsResult::default())
52 | }
53 | 
54 | // Notification that the list of roots has changed.
55 | //
56 | // 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.
57 | pub(crate) fn on_roots_list_changed(_input: PluginNotificationContext) -> Result<()> {
58 |     Ok(())
59 | }
60 | 
61 | // Read the contents of a resource by its URI.
62 | //
63 | // 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.
64 | pub(crate) fn read_resource(_input: ReadResourceRequest) -> Result<ReadResourceResult> {
65 |     Err(anyhow!("read_resource not implemented"))
66 | }
67 | 
```

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

```rust
 1 | mod pdk;
 2 | 
 3 | use base64::Engine;
 4 | use extism_pdk::*;
 5 | use pdk::types::*;
 6 | use serde_json::json;
 7 | use sha1::Sha1;
 8 | use sha2::{Digest, Sha224, Sha256, Sha384, Sha512};
 9 | 
10 | // Called when the tool is invoked.
11 | pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
12 |     extism_pdk::log!(
13 |         LogLevel::Info,
14 |         "called with args: {:?}",
15 |         input.params.arguments
16 |     );
17 |     let args = input.params.arguments.unwrap_or_default();
18 | 
19 |     let data = match args.get("data") {
20 |         Some(v) => v.as_str().unwrap(),
21 |         None => return Err(Error::msg("`data` is required")),
22 |     };
23 | 
24 |     let algorithm = match args.get("algorithm") {
25 |         Some(v) => v.as_str().unwrap(),
26 |         None => return Err(Error::msg("`algorithm` is required")),
27 |     };
28 | 
29 |     let result = match algorithm {
30 |         "sha256" => {
31 |             let mut hasher = Sha256::new();
32 |             hasher.update(data.as_bytes());
33 |             format!("{:x}", hasher.finalize())
34 |         }
35 |         "sha512" => {
36 |             let mut hasher = Sha512::new();
37 |             hasher.update(data.as_bytes());
38 |             format!("{:x}", hasher.finalize())
39 |         }
40 |         "sha384" => {
41 |             let mut hasher = Sha384::new();
42 |             hasher.update(data.as_bytes());
43 |             format!("{:x}", hasher.finalize())
44 |         }
45 |         "sha224" => {
46 |             let mut hasher = Sha224::new();
47 |             hasher.update(data.as_bytes());
48 |             format!("{:x}", hasher.finalize())
49 |         }
50 |         "sha1" => {
51 |             let mut hasher = Sha1::new();
52 |             hasher.update(data.as_bytes());
53 |             format!("{:x}", hasher.finalize())
54 |         }
55 |         "md5" => {
56 |             format!("{:x}", md5::compute(data))
57 |         }
58 |         "base32" => base32::encode(base32::Alphabet::RFC4648 { padding: true }, data.as_bytes()),
59 |         "base64" | _ => base64::engine::general_purpose::STANDARD.encode(data),
60 |     };
61 | 
62 |     Ok(CallToolResult {
63 |         is_error: None,
64 |         content: vec![Content {
65 |             annotations: None,
66 |             text: Some(result),
67 |             mime_type: Some("text/plain".into()),
68 |             r#type: ContentType::Text,
69 |             data: None,
70 |         }],
71 |     })
72 | }
73 | 
74 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
75 |     Ok(ListToolsResult {
76 |         tools: vec![ToolDescription {
77 |             name: "hash".into(),
78 |             description: "Hash data using various algorithms:  sha256, sha512, sha384, sha224, sha1, md5, base32, base64".into(),
79 |             input_schema: json!({
80 |                 "type": "object",
81 |                 "properties": {
82 |                     "data": {
83 |                         "type": "string",
84 |                         "description": "data to convert to hash or encoded format"
85 |                     },
86 |                     "algorithm": {
87 |                         "type": "string",
88 |                         "description": "algorithm to use for hashing or encoding",
89 |                         "enum": ["sha256", "sha512", "sha384", "sha224", "sha1", "md5", "base32", "base64"]
90 |                     }
91 |                 },
92 |                 "required": ["data", "algorithm"]
93 |             }).as_object().unwrap().clone(),
94 |         }],
95 |     })
96 | }
97 | 
```

--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------

```rust
 1 | mod cli;
 2 | mod config;
 3 | mod https_auth;
 4 | mod logging;
 5 | mod naming;
 6 | mod plugin;
 7 | mod service;
 8 | mod wasm;
 9 | 
10 | use anyhow::Result;
11 | use clap::Parser;
12 | use rmcp::transport::sse_server::SseServer;
13 | use rmcp::transport::streamable_http_server::{
14 |     StreamableHttpService, session::local::LocalSessionManager,
15 | };
16 | use rmcp::{ServiceExt, transport::stdio};
17 | use tokio::{runtime::Handle, task::block_in_place};
18 | 
19 | #[tokio::main]
20 | async fn main() -> Result<()> {
21 |     let cli = cli::Cli::parse();
22 |     let config = config::load_config(&cli).await?;
23 |     tracing::info!("Starting hyper-mcp server");
24 | 
25 |     match cli.transport.as_str() {
26 |         "stdio" => {
27 |             tracing::info!("Starting hyper-mcp with stdio transport");
28 |             let service = service::PluginService::new(&config)
29 |                 .await?
30 |                 .serve(stdio())
31 |                 .await
32 |                 .inspect_err(|e| {
33 |                     tracing::error!("Serving error: {:?}", e);
34 |                 })?;
35 |             service.waiting().await?;
36 |         }
37 |         "sse" => {
38 |             tracing::info!(
39 |                 "Starting hyper-mcp with SSE transport at {}",
40 |                 cli.bind_address
41 |             );
42 |             let ct = SseServer::serve(cli.bind_address.parse()?)
43 |                 .await?
44 |                 .with_service({
45 |                     move || {
46 |                         block_in_place(|| {
47 |                             Handle::current()
48 |                                 .block_on(async { service::PluginService::new(&config).await })
49 |                         })
50 |                         .expect("Failed to create plugin service")
51 |                     }
52 |                 });
53 | 
54 |             tokio::signal::ctrl_c().await?;
55 |             ct.cancel();
56 |         }
57 |         "streamable-http" => {
58 |             let bind_address = cli.bind_address.clone();
59 |             tracing::info!(
60 |                 "Starting hyper-mcp with streamable-http transport at {}/mcp",
61 |                 bind_address
62 |             );
63 | 
64 |             let service = StreamableHttpService::new(
65 |                 {
66 |                     move || {
67 |                         block_in_place(|| {
68 |                             Handle::current()
69 |                                 .block_on(async { service::PluginService::new(&config).await })
70 |                         })
71 |                         .map_err(std::io::Error::other)
72 |                     }
73 |                 },
74 |                 LocalSessionManager::default().into(),
75 |                 Default::default(),
76 |             );
77 | 
78 |             let router = axum::Router::new().nest_service("/mcp", service);
79 | 
80 |             let listener = tokio::net::TcpListener::bind(bind_address.clone()).await?;
81 | 
82 |             let _ = axum::serve(listener, router)
83 |                 .with_graceful_shutdown(async {
84 |                     tokio::signal::ctrl_c().await.unwrap();
85 |                     tracing::info!("Received Ctrl+C, shutting down hyper-mcp server...");
86 |                     // Give the log a moment to flush
87 |                     tokio::time::sleep(std::time::Duration::from_millis(100)).await;
88 |                     std::process::exit(0);
89 |                 })
90 |                 .await;
91 |         }
92 |         _ => unreachable!(),
93 |     }
94 | 
95 |     Ok(())
96 | }
97 | 
```

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

```rust
 1 | mod pdk;
 2 | 
 3 | use std::collections::BTreeMap;
 4 | 
 5 | use extism_pdk::*;
 6 | use json::Value;
 7 | use pdk::types::{
 8 |     CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
 9 | };
10 | use serde_json::json;
11 | 
12 | pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
13 |     match input.params.name.as_str() {
14 |         "serper_web_search" => serper_web_search(input),
15 |         _ => Ok(CallToolResult {
16 |             is_error: Some(true),
17 |             content: vec![Content {
18 |                 annotations: None,
19 |                 text: Some(format!("Unknown tool: {}", input.params.name)),
20 |                 mime_type: None,
21 |                 r#type: ContentType::Text,
22 |                 data: None,
23 |             }],
24 |         }),
25 |     }
26 | }
27 | 
28 | fn serper_web_search(input: CallToolRequest) -> Result<CallToolResult, Error> {
29 |     let args = input.params.arguments.unwrap_or_default();
30 |     let query = match args.get("q") {
31 |         Some(Value::String(q)) => q.clone(),
32 |         _ => {
33 |             return Ok(CallToolResult {
34 |                 is_error: Some(true),
35 |                 content: vec![Content {
36 |                     annotations: None,
37 |                     text: Some("Please provide a 'q' argument for the search query".into()),
38 |                     mime_type: None,
39 |                     r#type: ContentType::Text,
40 |                     data: None,
41 |                 }],
42 |             });
43 |         }
44 |     };
45 | 
46 |     let api_key = config::get("SERPER_API_KEY")?
47 |         .ok_or_else(|| Error::msg("SERPER_API_KEY configuration is required but not set"))?;
48 | 
49 |     let mut headers = BTreeMap::new();
50 |     headers.insert("X-API-KEY".to_string(), api_key);
51 |     headers.insert("Content-Type".to_string(), "application/json".to_string());
52 | 
53 |     let req = HttpRequest {
54 |         url: "https://google.serper.dev/search".to_string(),
55 |         headers,
56 |         method: Some("POST".to_string()),
57 |     };
58 | 
59 |     let body = json!({ "q": query });
60 |     let res = http::request(&req, Some(&body.to_string()))?;
61 |     let response_body = res.body();
62 |     let response_text = String::from_utf8_lossy(response_body.as_slice()).to_string();
63 | 
64 |     Ok(CallToolResult {
65 |         is_error: None,
66 |         content: vec![Content {
67 |             annotations: None,
68 |             text: Some(response_text),
69 |             mime_type: Some("application/json".to_string()),
70 |             r#type: ContentType::Text,
71 |             data: None,
72 |         }],
73 |     })
74 | }
75 | 
76 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
77 |     Ok(ListToolsResult{
78 |         tools: vec![
79 |             ToolDescription {
80 |                 name: "serper_web_search".into(),
81 |                 description:  "Performs a Google web search using the Serper API and returns the raw JSON response for the given query string.".into(),
82 |                 input_schema: json!({
83 |                     "type": "object",
84 |                     "properties": {
85 |                         "q": {
86 |                             "type": "string",
87 |                             "description": "The search query string",
88 |                         },
89 |                     },
90 |                     "required": ["q"],
91 |                 })
92 |                 .as_object()
93 |                 .unwrap()
94 |                 .clone(),
95 |             },
96 |         ],
97 |     })
98 | }
99 | 
```

--------------------------------------------------------------------------------
/tests/fixtures/skip_tools_examples.yaml:
--------------------------------------------------------------------------------

```yaml
  1 | # Test configuration file demonstrating various skip_tools functionality
  2 | plugins:
  3 |   # Plugin with exact tool name matches
  4 |   exact_match_plugin:
  5 |     url: "file:///path/to/exact_plugin"
  6 |     runtime_config:
  7 |       skip_tools:
  8 |         - "debug_tool"
  9 |         - "test_runner"
 10 |         - "deprecated_helper"
 11 | 
 12 |   # Plugin with wildcard patterns
 13 |   wildcard_plugin:
 14 |     url: "https://example.com/wildcard_plugin"
 15 |     runtime_config:
 16 |       skip_tools:
 17 |         - "temp_.*"       # Skip any tool starting with "temp_"
 18 |         - ".*_backup"     # Skip any tool ending with "_backup"
 19 |         - "debug.*"       # Skip any tool starting with "debug"
 20 | 
 21 |   # Plugin with complex regex patterns
 22 |   regex_plugin:
 23 |     url: "http://localhost:3000/regex_plugin"
 24 |     runtime_config:
 25 |       skip_tools:
 26 |         - "tool_[0-9]+"           # Skip tools like "tool_1", "tool_42"
 27 |         - "test_(unit|integration)" # Skip "test_unit" and "test_integration"
 28 |         - "[a-z]+_helper"         # Skip lowercase word followed by "_helper"
 29 | 
 30 |   # Plugin with anchored patterns (already have anchors)
 31 |   anchored_plugin:
 32 |     url: "file:///path/to/anchored_plugin"
 33 |     runtime_config:
 34 |       skip_tools:
 35 |         - "^system_.*"    # Explicit start anchor
 36 |         - ".*_internal$"  # Explicit end anchor
 37 |         - "^exact_only$"  # Fully anchored
 38 | 
 39 |   # Plugin with case-sensitive patterns
 40 |   case_sensitive_plugin:
 41 |     url: "https://api.example.com/case_plugin"
 42 |     runtime_config:
 43 |       skip_tools:
 44 |         - "Tool"          # Only matches "Tool", not "tool" or "TOOL"
 45 |         - "DEBUG_.*"      # Only matches uppercase DEBUG prefix
 46 |         - "CamelCase.*"   # Matches tools starting with "CamelCase"
 47 | 
 48 |   # Plugin with special regex characters escaped
 49 |   special_chars_plugin:
 50 |     url: "file:///path/to/special_plugin"
 51 |     runtime_config:
 52 |       skip_tools:
 53 |         - "file\\.exe"    # Matches "file.exe" literally
 54 |         - "script\\?"     # Matches "script?" literally
 55 |         - "temp\\*data"   # Matches "temp*data" literally
 56 |         - "path\\\\tool"  # Matches "path\tool" literally
 57 | 
 58 |   # Plugin with empty skip_tools (skip nothing)
 59 |   empty_skip_plugin:
 60 |     url: "https://example.com/empty_plugin"
 61 |     runtime_config:
 62 |       skip_tools: []
 63 |       allowed_hosts:
 64 |         - "example.com"
 65 | 
 66 |   # Plugin with no skip_tools specified (defaults to None)
 67 |   no_skip_plugin:
 68 |     url: "file:///path/to/no_skip_plugin"
 69 |     runtime_config:
 70 |       allowed_hosts:
 71 |         - "localhost"
 72 |       memory_limit: "512MB"
 73 | 
 74 |   # Plugin demonstrating mixed runtime config with skip_tools
 75 |   full_config_plugin:
 76 |     url: "https://secure.example.com/full_plugin"
 77 |     runtime_config:
 78 |       skip_tools:
 79 |         - "admin_.*"
 80 |         - ".*_dangerous"
 81 |         - "system_critical"
 82 |       allowed_hosts:
 83 |         - "api.example.com"
 84 |         - "cdn.example.com"
 85 |       allowed_paths:
 86 |         - "/tmp"
 87 |         - "/var/app/data"
 88 |       env_vars:
 89 |         ENVIRONMENT: "test"
 90 |         LOG_LEVEL: "debug"
 91 |       memory_limit: "2GB"
 92 | 
 93 |   # Plugin with common tool patterns to skip
 94 |   common_patterns_plugin:
 95 |     url: "file:///path/to/common_plugin"
 96 |     runtime_config:
 97 |       skip_tools:
 98 |         - ".*_test"       # Skip all test tools
 99 |         - "dev_.*"        # Skip all development tools
100 |         - "mock_.*"       # Skip all mock tools
101 |         - "stub_.*"       # Skip all stub tools
102 |         - ".*_deprecated" # Skip all deprecated tools
103 | 
```

--------------------------------------------------------------------------------
/CREATING_PLUGINS.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Creating Plugins
 2 | 
 3 | > **📌 Recommended: Use Plugin Templates**
 4 | >
 5 | > The fastest and easiest way to create a plugin is to use the provided templates. Templates include all necessary boilerplate, build configuration, and documentation out of the box.
 6 | >
 7 | > **[👉 Start with the Plugin Templates](./templates/plugins/README.md)**
 8 | 
 9 | Check out our [example plugins](https://github.com/tuananh/hyper-mcp/tree/main/examples/plugins/v2) for insight.
10 | 
11 | > Note: Prior versions of hyper-mcp used a different plugin interface (v1). While this plugin interface is still supported, new plugins should use the v2 interface.
12 | 
13 | ## Quick Start with Templates
14 | 
15 | The recommended way to create a new plugin:
16 | 
17 | 1. **Browse available templates** in [`templates/plugins/`](./templates/plugins/README.md)
18 | 
19 | 2. **Copy the template** for your language:
20 |    ```sh
21 |    cp -r templates/plugins/rust/ ../my-plugin/
22 |    cd ../my-plugin/
23 |    ```
24 | 
25 | 3. **Follow the template README** - each template includes comprehensive setup instructions, examples, and best practices
26 | 
27 | 4. **Customize and implement** your plugin logic
28 | 
29 | 5. **Build and publish** using the provided `Dockerfile`
30 | 
31 | See [Plugin Templates Documentation](./templates/plugins/README.md) for complete details and language options.
32 | 
33 | ## Using XTP (Alternative Method)
34 | 
35 | If you prefer to use the XTP CLI tool:
36 | 
37 | 1. Install the [XTP CLI](https://docs.xtp.dylibso.com/docs/cli):
38 |     ```sh
39 |     curl https://static.dylibso.com/cli/install.sh -s | bash
40 |     ```
41 | 
42 | 2. Create a new plugin project:
43 |     ```sh
44 |     xtp plugin init --schema-file xtp-plugin-schema.yaml
45 |     ```
46 |     Follow the prompts to set up your plugin. This will create the necessary files and structure.
47 | 
48 |     For example, if you chose Rust as the language, it will create a `Cargo.toml`, `src/lib.rs` and a `src/pdk.rs` file.
49 | 
50 | 3. Implement your plugin logic in the language appropriate files(s) created (e.g. - `Cargo.toml` and `src/lib.rs` for Rust)
51 |     For example, if you chose Rust as the language you will need to update the `Cargo.toml` and `src/lib.rs` files.
52 | 
53 |     Be sure to modify the `.gitignore` that is created for you to allow committing your `Cargo.lock` file.
54 | 
55 | ## Publishing Plugins
56 | 
57 | ### Rust
58 | 
59 | To publish a Rust plugin:
60 | 
61 | ```dockerfile
62 | # example how to build with rust
63 | FROM rust:1.88-slim AS builder
64 | 
65 | RUN rustup target add wasm32-wasip1 && \
66 |     rustup component add rust-std --target wasm32-wasip1 && \
67 |     cargo install cargo-auditable
68 | 
69 | WORKDIR /workspace
70 | COPY . .
71 | RUN cargo fetch
72 | RUN cargo auditable build --release --target wasm32-wasip1
73 | 
74 | FROM scratch
75 | WORKDIR /
76 | COPY --from=builder /workspace/target/wasm32-wasip1/release/plugin.wasm /plugin.wasm
77 | 
78 | ```
79 | 
80 | Then build and push:
81 | ```sh
82 | docker build -t your-registry/plugin-name .
83 | docker push your-registry/plugin-name
84 | ```
85 | 
86 | **Note:** The Rust template includes this Dockerfile and all necessary build configuration - no additional setup needed if you're using the template.
87 | 
88 | ## Next Steps
89 | 
90 | - **[📖 Plugin Templates Documentation](./templates/plugins/README.md)** - Comprehensive guide to using templates
91 | - **[🚀 Rust Plugin Template](./templates/plugins/rust/README.md)** - Complete Rust plugin setup and development guide
92 | - **[📚 Example Plugins](https://github.com/tuananh/hyper-mcp/tree/main/examples/plugins)** - Working examples to learn from
93 | - **[🔗 MCP Protocol Specification](https://spec.modelcontextprotocol.io/)** - Protocol details and specifications
94 | - **[⚙️ Extism Documentation](https://docs.extism.org/)** - Plugin runtime and PDK documentation
95 | 
```

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

```rust
  1 | mod pdk;
  2 | 
  3 | use std::collections::BTreeMap;
  4 | 
  5 | use extism_pdk::*;
  6 | use htmd::HtmlToMarkdown;
  7 | use json::Value;
  8 | use pdk::types::{
  9 |     CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
 10 | };
 11 | use serde_json::json;
 12 | 
 13 | pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
 14 |     match input.params.name.as_str() {
 15 |         "fetch" => fetch(input),
 16 |         _ => Ok(CallToolResult {
 17 |             is_error: Some(true),
 18 |             content: vec![Content {
 19 |                 annotations: None,
 20 |                 text: Some(format!("Unknown tool: {}", input.params.name)),
 21 |                 mime_type: None,
 22 |                 r#type: ContentType::Text,
 23 |                 data: None,
 24 |             }],
 25 |         }),
 26 |     }
 27 | }
 28 | 
 29 | fn fetch(input: CallToolRequest) -> Result<CallToolResult, Error> {
 30 |     let args = input.params.arguments.unwrap_or_default();
 31 |     if let Some(Value::String(url)) = args.get("url") {
 32 |         // Create HTTP request
 33 |         let mut req = HttpRequest {
 34 |             url: url.clone(),
 35 |             headers: BTreeMap::new(),
 36 |             method: Some("GET".to_string()),
 37 |         };
 38 | 
 39 |         // Add a user agent header to be polite
 40 |         req.headers
 41 |             .insert("User-Agent".to_string(), "fetch-tool/1.0".to_string());
 42 | 
 43 |         // Perform the request
 44 |         let res = http::request::<()>(&req, None)?;
 45 | 
 46 |         // Convert response body to string
 47 |         let body = res.body();
 48 |         let html = String::from_utf8_lossy(body.as_slice());
 49 | 
 50 |         let converter = HtmlToMarkdown::builder()
 51 |             .skip_tags(vec!["script", "style"])
 52 |             .build();
 53 | 
 54 |         // Convert HTML to markdown
 55 |         match converter.convert(&html) {
 56 |             Ok(markdown) => Ok(CallToolResult {
 57 |                 is_error: None,
 58 |                 content: vec![Content {
 59 |                     annotations: None,
 60 |                     text: Some(markdown),
 61 |                     mime_type: Some("text/markdown".to_string()),
 62 |                     r#type: ContentType::Text,
 63 |                     data: None,
 64 |                 }],
 65 |             }),
 66 |             Err(e) => Ok(CallToolResult {
 67 |                 is_error: Some(true),
 68 |                 content: vec![Content {
 69 |                     annotations: None,
 70 |                     text: Some(format!("Failed to convert HTML to markdown: {}", e)),
 71 |                     mime_type: None,
 72 |                     r#type: ContentType::Text,
 73 |                     data: None,
 74 |                 }],
 75 |             }),
 76 |         }
 77 |     } else {
 78 |         Ok(CallToolResult {
 79 |             is_error: Some(true),
 80 |             content: vec![Content {
 81 |                 annotations: None,
 82 |                 text: Some("Please provide a url".into()),
 83 |                 mime_type: None,
 84 |                 r#type: ContentType::Text,
 85 |                 data: None,
 86 |             }],
 87 |         })
 88 |     }
 89 | }
 90 | 
 91 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
 92 |     Ok(ListToolsResult{
 93 |         tools: vec![
 94 |             ToolDescription {
 95 |                 name: "fetch".into(),
 96 |                 description:  "Enables to open and access arbitrary text URLs. Fetches the contents of a URL and returns its contents converted to markdown".into(),
 97 |                 input_schema: json!({
 98 |                     "type": "object",
 99 |                     "properties": {
100 |                         "url": {
101 |                             "type": "string",
102 |                             "description": "The URL to fetch",
103 |                         },
104 |                     },
105 |                     "required": ["url"],
106 |                 })
107 |                 .as_object()
108 |                 .unwrap()
109 |                 .clone(),
110 |             },
111 |         ],
112 |     })
113 | }
114 | 
```

--------------------------------------------------------------------------------
/templates/plugins/go/main.go:
--------------------------------------------------------------------------------

```go
 1 | package main
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | )
 6 | 
 7 | // Execute a tool call. This is the primary entry point for tool execution in plugins.
 8 | //
 9 | // 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.
10 | // It takes CallToolRequest as input ()
11 | // And returns CallToolResult ()
12 | func CallTool(input CallToolRequest) (*CallToolResult, error) {
13 | 	return nil, fmt.Errorf("CallTool not implemented.")
14 | }
15 | 
16 | // Provide completion suggestions for a partially-typed input.
17 | //
18 | // 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.
19 | // It takes CompleteRequest as input ()
20 | // And returns CompleteResult ()
21 | func Complete(input CompleteRequest) (*CompleteResult, error) {
22 | 	return &CompleteResult{}, nil
23 | }
24 | 
25 | // Retrieve a specific prompt by name.
26 | //
27 | // This function is called when the user requests a specific prompt. The plugin should return the prompt details including messages and optional description.
28 | // It takes GetPromptRequest as input ()
29 | // And returns GetPromptResult ()
30 | func GetPrompt(input GetPromptRequest) (*GetPromptResult, error) {
31 | 	// TODO: fill out your implementation here
32 | 	return nil, fmt.Errorf("GetPrompt not implemented.")
33 | }
34 | 
35 | // List all available prompts.
36 | //
37 | // 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.
38 | // It takes ListPromptsRequest as input ()
39 | // And returns ListPromptsResult ()
40 | func ListPrompts(input ListPromptsRequest) (*ListPromptsResult, error) {
41 | 	// TODO: fill out your implementation here
42 | 	return &ListPromptsResult{}, nil
43 | }
44 | 
45 | // List all available resource templates.
46 | //
47 | // 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.
48 | // It takes ListResourceTemplatesRequest as input ()
49 | // And returns ListResourceTemplatesResult ()
50 | func ListResourceTemplates(input ListResourceTemplatesRequest) (*ListResourceTemplatesResult, error) {
51 | 	// TODO: fill out your implementation here
52 | 	return &ListResourceTemplatesResult{}, nil
53 | }
54 | 
55 | // List all available resources.
56 | //
57 | // 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.
58 | // It takes ListResourcesRequest as input ()
59 | // And returns ListResourcesResult ()
60 | func ListResources(input ListResourcesRequest) (*ListResourcesResult, error) {
61 | 	// TODO: fill out your implementation here
62 | 	return &ListResourcesResult{}, nil
63 | }
64 | 
65 | // List all available tools.
66 | //
67 | // 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.
68 | // It takes ListToolsRequest as input ()
69 | // And returns ListToolsResult ()
70 | func ListTools(input ListToolsRequest) (*ListToolsResult, error) {
71 | 	// TODO: fill out your implementation here
72 | 	return &ListToolsResult{}, nil
73 | }
74 | 
75 | // Notification that the list of roots has changed.
76 | //
77 | // 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.
78 | // It takes PluginNotificationContext as input ()
79 | func OnRootsListChanged(input PluginNotificationContext) error {
80 | 	// TODO: fill out your implementation here
81 | 	return nil
82 | }
83 | 
84 | // Read the contents of a resource by its URI.
85 | //
86 | // 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.
87 | // It takes ReadResourceRequest as input ()
88 | // And returns ReadResourceResult ()
89 | func ReadResource(input ReadResourceRequest) (*ReadResourceResult, error) {
90 | 	// TODO: fill out your implementation here
91 | 	return nil, fmt.Errorf("ReadResource not implemented.")
92 | }
93 | 
94 | // Note: leave this in place, as the Go compiler will find the `export` function as the entrypoint.
95 | func main() {}
96 | 
```

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

```rust
  1 | mod pdk;
  2 | 
  3 | use extism_pdk::*;
  4 | use pdk::types::{CallToolResult, Content, ContentType, ToolDescription};
  5 | use pdk::*;
  6 | use serde_json::json;
  7 | use std::error::Error as StdError;
  8 | 
  9 | use chrono::Utc;
 10 | 
 11 | #[derive(Debug)]
 12 | struct CustomError(String);
 13 | 
 14 | impl std::fmt::Display for CustomError {
 15 |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 16 |         write!(f, "{}", self.0)
 17 |     }
 18 | }
 19 | 
 20 | impl StdError for CustomError {}
 21 | 
 22 | // Called when the tool is invoked.
 23 | pub(crate) fn call(input: types::CallToolRequest) -> Result<types::CallToolResult, Error> {
 24 |     let args = input.params.arguments.unwrap_or_default();
 25 |     let name = args.get("name").unwrap().as_str().unwrap();
 26 |     match name {
 27 |         "get_time_utc" => {
 28 |             let now = Utc::now();
 29 |             let timestamp = now.timestamp().to_string();
 30 |             let rfc2822 = now.to_rfc2822().to_string();
 31 |             Ok(CallToolResult {
 32 |                 content: vec![Content {
 33 |                     text: Some(
 34 |                         json!({
 35 |                             "utc_time": timestamp,
 36 |                             "utc_time_rfc2822": rfc2822,
 37 |                         })
 38 |                         .to_string(),
 39 |                     ),
 40 |                     r#type: ContentType::Text,
 41 |                     ..Default::default()
 42 |                 }],
 43 |                 is_error: Some(false),
 44 |             })
 45 |         }
 46 |         "parse_time" => {
 47 |             let time = args.get("time_rfc2822").unwrap().as_str().unwrap();
 48 |             let t = chrono::DateTime::parse_from_rfc2822(time).unwrap();
 49 |             let timestamp = t.timestamp().to_string();
 50 |             let rfc2822 = t.to_rfc2822().to_string();
 51 |             Ok(CallToolResult {
 52 |                 content: vec![Content {
 53 |                     text: Some(
 54 |                         json!({
 55 |                             "utc_time": timestamp,
 56 |                             "utc_time_rfc2822": rfc2822,
 57 |                         })
 58 |                         .to_string(),
 59 |                     ),
 60 |                     r#type: ContentType::Text,
 61 |                     ..Default::default()
 62 |                 }],
 63 |                 is_error: Some(false),
 64 |             })
 65 |         }
 66 |         "time_offset" => {
 67 |             let t1 = args.get("timestamp").unwrap().as_i64().unwrap();
 68 |             let offset = args.get("offset").unwrap().as_i64().unwrap();
 69 |             let t1 = chrono::DateTime::from_timestamp(t1, 0).unwrap();
 70 |             let t2 = t1 + chrono::Duration::seconds(offset);
 71 |             let timestamp = t2.timestamp().to_string();
 72 |             let rfc2822 = t2.to_rfc2822().to_string();
 73 |             Ok(CallToolResult {
 74 |                 content: vec![Content {
 75 |                     text: Some(
 76 |                         json!({
 77 |                             "utc_time": timestamp,
 78 |                             "utc_time_rfc2822": rfc2822,
 79 |                         })
 80 |                         .to_string(),
 81 |                     ),
 82 |                     r#type: ContentType::Text,
 83 |                     ..Default::default()
 84 |                 }],
 85 |                 is_error: Some(false),
 86 |             })
 87 |         }
 88 |         _ => Err(Error::new(CustomError("unknown command".to_string()))),
 89 |     }
 90 | }
 91 | 
 92 | pub(crate) fn describe() -> Result<types::ListToolsResult, Error> {
 93 |     Ok(types::ListToolsResult { tools: vec![ToolDescription {
 94 |         name: "time".into(),
 95 |         description: "Time operations plugin. It provides the following operations:
 96 | 
 97 | - `get_time_utc`: Returns the current time in the UTC timezone. Takes no parameters.
 98 | - `parse_time`: Takes a `time_rfc2822` string in RFC2822 format and returns the timestamp in UTC timezone.
 99 | - `time_offset`: Takes integer `timestamp` and `offset` parameters. Adds a time offset to a given timestamp and returns the new timestamp in UTC timezone.
100 | 
101 | Always use this tool to compute time operations, especially when it is necessary
102 | to compute time differences or offsets.".into(),
103 |         input_schema: json!({
104 |             "type": "object",
105 |             "required": ["name"],
106 |             "properties": {
107 |                 "name": {
108 |                     "type": "string",
109 |                     "description": "The name of the operation to perform. ",
110 |                     "enum": ["get_time_utc", "time_offset",  "parse_time"],
111 |                 },
112 |                 "timestamp": {
113 |                     "type": "integer",
114 |                     "description": "The timestamp used for `time_offset`.",
115 |                 },
116 |                 "offset" : {
117 |                     "type": "integer",
118 |                     "description": "The offset to add to the time in seconds. ",
119 |                 },
120 |                 "time_rfc2822": {
121 |                     "type": "string",
122 |                     "description": "The time in RFC2822 format used in `parse_time`",
123 |                 },
124 |             },
125 |         })
126 |         .as_object()
127 |         .unwrap()
128 |         .clone(),
129 |     }]})
130 | }
131 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v2/rstime/src/pdk/imports.rs:
--------------------------------------------------------------------------------

```rust
  1 | #![allow(unused)]
  2 | use super::types::*;
  3 | use extism_pdk::{Error, Json, host_fn};
  4 | use std::result::Result;
  5 | 
  6 | /// create_elicitation Request user input through the client's elicitation interface.
  7 | ///
  8 | /// 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.
  9 | /// It takes input of CreateElicitationRequestParamWithTimeout ()
 10 | /// And it returns an output CreateElicitationResult ()
 11 | pub(crate) fn create_elicitation(
 12 |     input: ElicitRequestParamWithTimeout,
 13 | ) -> Result<ElicitResult, Error> {
 14 |     let Json(res) = unsafe { raw_imports::create_elicitation(Json(input))? };
 15 | 
 16 |     Ok(res)
 17 | }
 18 | 
 19 | /// create_message Request message creation through the client's sampling interface.
 20 | ///
 21 | /// 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.
 22 | /// It takes input of CreateMessageRequestParam ()
 23 | /// And it returns an output CreateMessageResult ()
 24 | #[allow(unused)]
 25 | pub(crate) fn create_message(
 26 |     input: CreateMessageRequestParam,
 27 | ) -> Result<CreateMessageResult, Error> {
 28 |     let Json(res) = unsafe { raw_imports::create_message(Json(input))? };
 29 | 
 30 |     Ok(res)
 31 | }
 32 | 
 33 | /// list_roots List the client's root directories or resources.
 34 | ///
 35 | /// 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.
 36 | /// And it returns an output ListRootsResult ()
 37 | pub(crate) fn list_roots() -> Result<ListRootsResult, Error> {
 38 |     let Json(res) = unsafe { raw_imports::list_roots()? };
 39 | 
 40 |     Ok(res)
 41 | }
 42 | 
 43 | /// notify_logging_message Send a logging message to the client.
 44 | ///
 45 | /// Plugins use this to report diagnostic, informational, warning, or error messages. The client's logging level determines which messages are processed.
 46 | /// It takes input of LoggingMessageNotificationParam ()
 47 | pub(crate) fn notify_logging_message(input: LoggingMessageNotificationParam) -> Result<(), Error> {
 48 |     unsafe { raw_imports::notify_logging_message(Json(input))? }
 49 |     Ok(())
 50 | }
 51 | 
 52 | /// notify_progress Send a progress notification to the client.
 53 | ///
 54 | /// Plugins use this to report progress during long-running operations. This allows clients to display progress bars or status information to users.
 55 | /// It takes input of ProgressNotificationParam ()
 56 | pub(crate) fn notify_progress(input: ProgressNotificationParam) -> Result<(), Error> {
 57 |     unsafe { raw_imports::notify_progress(Json(input))? }
 58 |     Ok(())
 59 | }
 60 | 
 61 | /// notify_prompt_list_changed Notify the client that the list of available prompts has changed.
 62 | ///
 63 | /// Plugins should call this when they add, remove, or modify their available prompts. The client will typically refresh its prompt list in response.
 64 | pub(crate) fn notify_prompt_list_changed() -> Result<(), Error> {
 65 |     unsafe { raw_imports::notify_prompt_list_changed()? }
 66 |     Ok(())
 67 | }
 68 | 
 69 | /// notify_resource_list_changed Notify the client that the list of available resources has changed.
 70 | ///
 71 | /// Plugins should call this when they add, remove, or modify their available resources. The client will typically refresh its resource list in response.
 72 | pub(crate) fn notify_resource_list_changed() -> Result<(), Error> {
 73 |     unsafe { raw_imports::notify_resource_list_changed()? }
 74 |     Ok(())
 75 | }
 76 | 
 77 | /// notify_resource_updated Notify the client that a specific resource has been updated.
 78 | ///
 79 | /// Plugins should call this when they modify the contents of a resource. The client can use this to invalidate caches and refresh resource displays.
 80 | /// It takes input of types::ResourceUpdatedNotificationParam ()
 81 | pub(crate) fn notify_resource_updated(
 82 |     input: ResourceUpdatedNotificationParam,
 83 | ) -> Result<(), Error> {
 84 |     unsafe { raw_imports::notify_resource_updated(Json(input))? }
 85 |     Ok(())
 86 | }
 87 | 
 88 | /// notify_tool_list_changed Notify the client that the list of available tools has changed.
 89 | ///
 90 | /// Plugins should call this when they add, remove, or modify their available tools. The client will typically refresh its tool list in response.
 91 | pub(crate) fn notify_tool_list_changed() -> Result<(), Error> {
 92 |     unsafe { raw_imports::notify_tool_list_changed()? }
 93 |     Ok(())
 94 | }
 95 | 
 96 | mod raw_imports {
 97 |     use super::*;
 98 |     #[host_fn]
 99 |     extern "ExtismHost" {
100 |         pub(crate) fn create_elicitation(
101 |             input: Json<ElicitRequestParamWithTimeout>,
102 |         ) -> Json<ElicitResult>;
103 | 
104 |         pub(crate) fn create_message(
105 |             input: Json<CreateMessageRequestParam>,
106 |         ) -> Json<CreateMessageResult>;
107 | 
108 |         pub(crate) fn list_roots() -> Json<ListRootsResult>;
109 | 
110 |         pub(crate) fn notify_logging_message(input: Json<LoggingMessageNotificationParam>);
111 | 
112 |         pub(crate) fn notify_progress(input: Json<ProgressNotificationParam>);
113 | 
114 |         pub(crate) fn notify_prompt_list_changed();
115 | 
116 |         pub(crate) fn notify_resource_list_changed();
117 | 
118 |         pub(crate) fn notify_resource_updated(input: Json<ResourceUpdatedNotificationParam>);
119 | 
120 |         pub(crate) fn notify_tool_list_changed();
121 |     }
122 | }
123 | 
```

--------------------------------------------------------------------------------
/templates/plugins/rust/src/pdk/imports.rs:
--------------------------------------------------------------------------------

```rust
  1 | #![allow(unused)]
  2 | use super::types::*;
  3 | use extism_pdk::{Error, Json, host_fn};
  4 | use std::result::Result;
  5 | 
  6 | /// create_elicitation Request user input through the client's elicitation interface.
  7 | ///
  8 | /// 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.
  9 | /// It takes input of CreateElicitationRequestParamWithTimeout ()
 10 | /// And it returns an output CreateElicitationResult ()
 11 | pub(crate) fn create_elicitation(
 12 |     input: ElicitRequestParamWithTimeout,
 13 | ) -> Result<ElicitResult, Error> {
 14 |     let Json(res) = unsafe { raw_imports::create_elicitation(Json(input))? };
 15 | 
 16 |     Ok(res)
 17 | }
 18 | 
 19 | /// create_message Request message creation through the client's sampling interface.
 20 | ///
 21 | /// 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.
 22 | /// It takes input of CreateMessageRequestParam ()
 23 | /// And it returns an output CreateMessageResult ()
 24 | #[allow(unused)]
 25 | pub(crate) fn create_message(
 26 |     input: CreateMessageRequestParam,
 27 | ) -> Result<CreateMessageResult, Error> {
 28 |     let Json(res) = unsafe { raw_imports::create_message(Json(input))? };
 29 | 
 30 |     Ok(res)
 31 | }
 32 | 
 33 | /// list_roots List the client's root directories or resources.
 34 | ///
 35 | /// 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.
 36 | /// And it returns an output ListRootsResult ()
 37 | pub(crate) fn list_roots() -> Result<ListRootsResult, Error> {
 38 |     let Json(res) = unsafe { raw_imports::list_roots()? };
 39 | 
 40 |     Ok(res)
 41 | }
 42 | 
 43 | /// notify_logging_message Send a logging message to the client.
 44 | ///
 45 | /// Plugins use this to report diagnostic, informational, warning, or error messages. The client's logging level determines which messages are processed.
 46 | /// It takes input of LoggingMessageNotificationParam ()
 47 | pub(crate) fn notify_logging_message(input: LoggingMessageNotificationParam) -> Result<(), Error> {
 48 |     unsafe { raw_imports::notify_logging_message(Json(input))? }
 49 |     Ok(())
 50 | }
 51 | 
 52 | /// notify_progress Send a progress notification to the client.
 53 | ///
 54 | /// Plugins use this to report progress during long-running operations. This allows clients to display progress bars or status information to users.
 55 | /// It takes input of ProgressNotificationParam ()
 56 | pub(crate) fn notify_progress(input: ProgressNotificationParam) -> Result<(), Error> {
 57 |     unsafe { raw_imports::notify_progress(Json(input))? }
 58 |     Ok(())
 59 | }
 60 | 
 61 | /// notify_prompt_list_changed Notify the client that the list of available prompts has changed.
 62 | ///
 63 | /// Plugins should call this when they add, remove, or modify their available prompts. The client will typically refresh its prompt list in response.
 64 | pub(crate) fn notify_prompt_list_changed() -> Result<(), Error> {
 65 |     unsafe { raw_imports::notify_prompt_list_changed()? }
 66 |     Ok(())
 67 | }
 68 | 
 69 | /// notify_resource_list_changed Notify the client that the list of available resources has changed.
 70 | ///
 71 | /// Plugins should call this when they add, remove, or modify their available resources. The client will typically refresh its resource list in response.
 72 | pub(crate) fn notify_resource_list_changed() -> Result<(), Error> {
 73 |     unsafe { raw_imports::notify_resource_list_changed()? }
 74 |     Ok(())
 75 | }
 76 | 
 77 | /// notify_resource_updated Notify the client that a specific resource has been updated.
 78 | ///
 79 | /// Plugins should call this when they modify the contents of a resource. The client can use this to invalidate caches and refresh resource displays.
 80 | /// It takes input of types::ResourceUpdatedNotificationParam ()
 81 | pub(crate) fn notify_resource_updated(
 82 |     input: ResourceUpdatedNotificationParam,
 83 | ) -> Result<(), Error> {
 84 |     unsafe { raw_imports::notify_resource_updated(Json(input))? }
 85 |     Ok(())
 86 | }
 87 | 
 88 | /// notify_tool_list_changed Notify the client that the list of available tools has changed.
 89 | ///
 90 | /// Plugins should call this when they add, remove, or modify their available tools. The client will typically refresh its tool list in response.
 91 | pub(crate) fn notify_tool_list_changed() -> Result<(), Error> {
 92 |     unsafe { raw_imports::notify_tool_list_changed()? }
 93 |     Ok(())
 94 | }
 95 | 
 96 | mod raw_imports {
 97 |     use super::*;
 98 |     #[host_fn]
 99 |     extern "ExtismHost" {
100 |         pub(crate) fn create_elicitation(
101 |             input: Json<ElicitRequestParamWithTimeout>,
102 |         ) -> Json<ElicitResult>;
103 | 
104 |         pub(crate) fn create_message(
105 |             input: Json<CreateMessageRequestParam>,
106 |         ) -> Json<CreateMessageResult>;
107 | 
108 |         pub(crate) fn list_roots() -> Json<ListRootsResult>;
109 | 
110 |         pub(crate) fn notify_logging_message(input: Json<LoggingMessageNotificationParam>);
111 | 
112 |         pub(crate) fn notify_progress(input: Json<ProgressNotificationParam>);
113 | 
114 |         pub(crate) fn notify_prompt_list_changed();
115 | 
116 |         pub(crate) fn notify_resource_list_changed();
117 | 
118 |         pub(crate) fn notify_resource_updated(input: Json<ResourceUpdatedNotificationParam>);
119 | 
120 |         pub(crate) fn notify_tool_list_changed();
121 |     }
122 | }
123 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/eval-py/src/lib.rs:
--------------------------------------------------------------------------------

```rust
  1 | mod pdk;
  2 | 
  3 | use rustpython_vm::{self as vm, Settings, scope::Scope};
  4 | use std::{cell::RefCell, collections::HashMap, rc::Rc};
  5 | 
  6 | use extism_pdk::*;
  7 | use json::Value;
  8 | use pdk::types::{
  9 |     CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
 10 | };
 11 | use serde_json::json;
 12 | 
 13 | struct StoredVirtualMachine {
 14 |     interp: vm::Interpreter,
 15 |     scope: Scope,
 16 | }
 17 | 
 18 | impl StoredVirtualMachine {
 19 |     fn new() -> Self {
 20 |         let mut scope = None;
 21 |         let mut settings = Settings::default();
 22 |         settings.allow_external_library = false;
 23 | 
 24 |         let interp = vm::Interpreter::with_init(settings, |vm| {
 25 |             scope = Some(vm.new_scope_with_builtins());
 26 |         });
 27 | 
 28 |         StoredVirtualMachine {
 29 |             interp,
 30 |             scope: scope.unwrap(),
 31 |         }
 32 |     }
 33 | }
 34 | 
 35 | thread_local! {
 36 |     static STORED_VMS: RefCell<HashMap<String, Rc<StoredVirtualMachine>>> = RefCell::default();
 37 | }
 38 | 
 39 | fn get_or_create_vm(id: &str) -> Rc<StoredVirtualMachine> {
 40 |     STORED_VMS.with(|cell| {
 41 |         let mut vms = cell.borrow_mut();
 42 |         if !vms.contains_key(id) {
 43 |             let stored_vm = StoredVirtualMachine::new();
 44 |             vms.insert(id.to_string(), Rc::new(stored_vm));
 45 |         }
 46 |         vms.get(id).unwrap().clone()
 47 |     })
 48 | }
 49 | 
 50 | pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
 51 |     match input.params.name.as_str() {
 52 |         "eval_python" => eval_python(input),
 53 |         _ => Ok(CallToolResult {
 54 |             is_error: Some(true),
 55 |             content: vec![Content {
 56 |                 annotations: None,
 57 |                 text: Some(format!("Unknown tool: {}", input.params.name)),
 58 |                 mime_type: None,
 59 |                 r#type: ContentType::Text,
 60 |                 data: None,
 61 |             }],
 62 |         }),
 63 |     }
 64 | }
 65 | 
 66 | fn eval_python(input: CallToolRequest) -> Result<CallToolResult, Error> {
 67 |     let args = input.params.arguments.unwrap_or_default();
 68 |     if let Some(Value::String(code)) = args.get("code") {
 69 |         let stored_vm = get_or_create_vm("eval_python");
 70 | 
 71 |         let result = stored_vm.interp.enter(|vm| {
 72 |             match vm
 73 |                 .compile(code, vm::compiler::Mode::Single, "<eval>".to_owned())
 74 |                 .map_err(|err| vm.new_syntax_error(&err, Some(code)))
 75 |                 .and_then(|code_obj| vm.run_code_obj(code_obj, stored_vm.scope.clone()))
 76 |             {
 77 |                 Ok(output) => {
 78 |                     if !vm.is_none(&output) {
 79 |                         stored_vm
 80 |                             .scope
 81 |                             .globals
 82 |                             .set_item("last", output.clone(), vm)?;
 83 | 
 84 |                         match output.str(vm) {
 85 |                             Ok(s) => Ok(s.to_string()),
 86 |                             Err(e) => Err(e),
 87 |                         }
 88 |                     } else {
 89 |                         Ok("None".to_string())
 90 |                     }
 91 |                 }
 92 |                 Err(exc) => Err(exc),
 93 |             }
 94 |         });
 95 | 
 96 |         match result {
 97 |             Ok(output) => Ok(CallToolResult {
 98 |                 is_error: None,
 99 |                 content: vec![Content {
100 |                     annotations: None,
101 |                     text: Some(output),
102 |                     mime_type: Some("text/plain".to_string()),
103 |                     r#type: ContentType::Text,
104 |                     data: None,
105 |                 }],
106 |             }),
107 |             Err(exc) => {
108 |                 let mut error_msg = String::new();
109 |                 stored_vm.interp.enter(|vm| {
110 |                     vm.write_exception(&mut error_msg, &exc).unwrap_or_default();
111 |                 });
112 |                 Ok(CallToolResult {
113 |                     is_error: Some(true),
114 |                     content: vec![Content {
115 |                         annotations: None,
116 |                         text: Some(error_msg),
117 |                         mime_type: None,
118 |                         r#type: ContentType::Text,
119 |                         data: None,
120 |                     }],
121 |                 })
122 |             }
123 |         }
124 |     } else {
125 |         Ok(CallToolResult {
126 |             is_error: Some(true),
127 |             content: vec![Content {
128 |                 annotations: None,
129 |                 text: Some("Please provide Python code to evaluate".into()),
130 |                 mime_type: None,
131 |                 r#type: ContentType::Text,
132 |                 data: None,
133 |             }],
134 |         })
135 |     }
136 | }
137 | 
138 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
139 |     Ok(ListToolsResult{
140 |         tools: vec![
141 |             ToolDescription {
142 |                 name: "eval_python".into(),
143 |                 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(),
144 |                 input_schema: json!({
145 |                     "type": "object",
146 |                     "properties": {
147 |                         "code": {
148 |                             "type": "string",
149 |                             "description": "The Python code to evaluate",
150 |                         },
151 |                     },
152 |                     "required": ["code"],
153 |                 })
154 |                 .as_object()
155 |                 .unwrap()
156 |                 .clone(),
157 |             },
158 |         ],
159 |     })
160 | }
161 | 
```

--------------------------------------------------------------------------------
/templates/plugins/go/imports.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import pdk "github.com/extism/go-pdk"
  4 | 
  5 | // CreateElicitation Request user input through the client's elicitation interface.
  6 | //
  7 | // 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.
  8 | // It takes input of CreateElicitationRequestParamWithTimeout ()
  9 | // And it returns an output *CreateElicitationResult ()
 10 | func CreateElicitation(input ElicitRequestParamWithTimeout) (*ElicitResult, error) {
 11 | 	var err error
 12 | 	_ = err
 13 | 	mem, err := pdk.AllocateJSON(&input)
 14 | 	if err != nil {
 15 | 		return nil, err
 16 | 	}
 17 | 
 18 | 	offs := _CreateElicitation(mem.Offset())
 19 | 
 20 | 	var out ElicitResult
 21 | 	err = pdk.JSONFrom(offs, &out)
 22 | 	if err != nil {
 23 | 		return nil, err
 24 | 	}
 25 | 	return &out, nil
 26 | 
 27 | }
 28 | 
 29 | // CreateMessage Request message creation through the client's sampling interface.
 30 | //
 31 | // 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.
 32 | // It takes input of CreateMessageRequestParam ()
 33 | // And it returns an output *CreateMessageResult ()
 34 | func CreateMessage(input CreateMessageRequestParam) (*CreateMessageResult, error) {
 35 | 	var err error
 36 | 	_ = err
 37 | 	mem, err := pdk.AllocateJSON(&input)
 38 | 	if err != nil {
 39 | 		return nil, err
 40 | 	}
 41 | 
 42 | 	offs := _CreateMessage(mem.Offset())
 43 | 
 44 | 	var out CreateMessageResult
 45 | 	err = pdk.JSONFrom(offs, &out)
 46 | 	if err != nil {
 47 | 		return nil, err
 48 | 	}
 49 | 	return &out, nil
 50 | 
 51 | }
 52 | 
 53 | // ListRoots List the client's root directories or resources.
 54 | //
 55 | // 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.
 56 | // And it returns an output *ListRootsResult ()
 57 | func ListRoots() (*ListRootsResult, error) {
 58 | 	var err error
 59 | 	_ = err
 60 | 	offs := _ListRoots()
 61 | 
 62 | 	var out ListRootsResult
 63 | 	err = pdk.JSONFrom(offs, &out)
 64 | 	if err != nil {
 65 | 		return nil, err
 66 | 	}
 67 | 	return &out, nil
 68 | 
 69 | }
 70 | 
 71 | // NotifyLoggingMessage Send a logging message to the client.
 72 | //
 73 | // Plugins use this to report diagnostic, informational, warning, or error messages. The client's logging level determines which messages are processed.
 74 | // It takes input of LoggingMessageNotificationParam ()
 75 | func NotifyLoggingMessage(input LoggingMessageNotificationParam) error {
 76 | 	var err error
 77 | 	_ = err
 78 | 	mem, err := pdk.AllocateJSON(&input)
 79 | 	if err != nil {
 80 | 		return err
 81 | 	}
 82 | 
 83 | 	_NotifyLoggingMessage(mem.Offset())
 84 | 
 85 | 	return nil
 86 | 
 87 | }
 88 | 
 89 | // NotifyProgress Send a progress notification to the client.
 90 | //
 91 | // Plugins use this to report progress during long-running operations. This allows clients to display progress bars or status information to users.
 92 | // It takes input of ProgressNotificationParam ()
 93 | func NotifyProgress(input ProgressNotificationParam) error {
 94 | 	var err error
 95 | 	_ = err
 96 | 	mem, err := pdk.AllocateJSON(&input)
 97 | 	if err != nil {
 98 | 		return err
 99 | 	}
100 | 
101 | 	_NotifyProgress(mem.Offset())
102 | 
103 | 	return nil
104 | 
105 | }
106 | 
107 | // NotifyPromptListChanged Notify the client that the list of available prompts has changed.
108 | //
109 | // Plugins should call this when they add, remove, or modify their available prompts. The client will typically refresh its prompt list in response.
110 | func NotifyPromptListChanged() error {
111 | 	var err error
112 | 	_ = err
113 | 	_NotifyPromptListChanged()
114 | 
115 | 	return nil
116 | 
117 | }
118 | 
119 | // NotifyResourceListChanged Notify the client that the list of available resources has changed.
120 | //
121 | // Plugins should call this when they add, remove, or modify their available resources. The client will typically refresh its resource list in response.
122 | func NotifyResourceListChanged() error {
123 | 	var err error
124 | 	_ = err
125 | 	_NotifyResourceListChanged()
126 | 
127 | 	return nil
128 | 
129 | }
130 | 
131 | // NotifyResourceUpdated Notify the client that a specific resource has been updated.
132 | //
133 | // Plugins should call this when they modify the contents of a resource. The client can use this to invalidate caches and refresh resource displays.
134 | // It takes input of ResourceUpdatedNotificationParam ()
135 | func NotifyResourceUpdated(input ResourceUpdatedNotificationParam) error {
136 | 	var err error
137 | 	_ = err
138 | 	mem, err := pdk.AllocateJSON(&input)
139 | 	if err != nil {
140 | 		return err
141 | 	}
142 | 
143 | 	_NotifyResourceUpdated(mem.Offset())
144 | 
145 | 	return nil
146 | 
147 | }
148 | 
149 | // NotifyToolListChanged Notify the client that the list of available tools has changed.
150 | //
151 | // Plugins should call this when they add, remove, or modify their available tools. The client will typically refresh its tool list in response.
152 | func NotifyToolListChanged() error {
153 | 	var err error
154 | 	_ = err
155 | 	_NotifyToolListChanged()
156 | 
157 | 	return nil
158 | 
159 | }
160 | 
161 | //go:wasmimport extism:host/user create_elicitation
162 | func _CreateElicitation(uint64) uint64
163 | 
164 | //go:wasmimport extism:host/user create_message
165 | func _CreateMessage(uint64) uint64
166 | 
167 | //go:wasmimport extism:host/user list_roots
168 | func _ListRoots() uint64
169 | 
170 | //go:wasmimport extism:host/user notify_logging_message
171 | func _NotifyLoggingMessage(uint64)
172 | 
173 | //go:wasmimport extism:host/user notify_progress
174 | func _NotifyProgress(uint64)
175 | 
176 | //go:wasmimport extism:host/user notify_prompt_list_changed
177 | func _NotifyPromptListChanged()
178 | 
179 | //go:wasmimport extism:host/user notify_resource_list_changed
180 | func _NotifyResourceListChanged()
181 | 
182 | //go:wasmimport extism:host/user notify_resource_updated
183 | func _NotifyResourceUpdated(uint64)
184 | 
185 | //go:wasmimport extism:host/user notify_tool_list_changed
186 | func _NotifyToolListChanged()
187 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/tool-list-changed/src/lib.rs:
--------------------------------------------------------------------------------

```rust
  1 | mod pdk;
  2 | 
  3 | use extism_pdk::*;
  4 | use pdk::types::{CallToolResult, Content, ContentType, ListToolsResult, ToolDescription};
  5 | use pdk::*;
  6 | use serde_json::json;
  7 | use std::error::Error as StdError;
  8 | use std::sync::atomic::{AtomicUsize, Ordering};
  9 | 
 10 | #[derive(Debug)]
 11 | struct CustomError(String);
 12 | 
 13 | impl std::fmt::Display for CustomError {
 14 |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 15 |         write!(f, "{}", self.0)
 16 |     }
 17 | }
 18 | 
 19 | impl StdError for CustomError {}
 20 | 
 21 | // Global counter to track how many tools have been added
 22 | static TOOL_COUNT: AtomicUsize = AtomicUsize::new(0);
 23 | 
 24 | #[host_fn("extism:host/user")]
 25 | extern "ExtismHost" {
 26 |     fn notify_tool_list_changed();
 27 | }
 28 | 
 29 | // Called when a tool is invoked
 30 | pub(crate) fn call(input: types::CallToolRequest) -> Result<types::CallToolResult, Error> {
 31 |     let tool_name = &input.params.name;
 32 | 
 33 |     match tool_name.as_str() {
 34 |         "add_tool" => {
 35 |             // Increment the tool count
 36 |             let new_count = TOOL_COUNT.fetch_add(1, Ordering::SeqCst) + 1;
 37 | 
 38 |             // Notify that the tool list has changed
 39 |             match unsafe { notify_tool_list_changed() } {
 40 |                 Ok(()) => Ok(CallToolResult {
 41 |                     content: vec![Content {
 42 |                         text: Some(
 43 |                             json!({
 44 |                                 "message": format!("Successfully added tool_{}", new_count),
 45 |                                 "tool_count": new_count,
 46 |                             })
 47 |                             .to_string(),
 48 |                         ),
 49 |                         r#type: ContentType::Text,
 50 |                         ..Default::default()
 51 |                     }],
 52 |                     is_error: Some(false),
 53 |                 }),
 54 |                 Err(e) => Ok(CallToolResult {
 55 |                     content: vec![Content {
 56 |                         text: Some(format!("Failed to notify host of tool list change: {}", e)),
 57 |                         r#type: ContentType::Text,
 58 |                         ..Default::default()
 59 |                     }],
 60 |                     is_error: Some(true),
 61 |                 }),
 62 |             }
 63 |         }
 64 |         tool_name if tool_name.starts_with("tool_") => {
 65 |             // Handle dynamically created tools
 66 |             let tool_number = tool_name.strip_prefix("tool_").unwrap_or("unknown");
 67 | 
 68 |             // Validate that the tool exists by comparing to TOOL_COUNT
 69 |             if let Ok(number_str) = std::str::from_utf8(tool_number.as_bytes()) {
 70 |                 if let Ok(tool_num) = number_str.parse::<usize>() {
 71 |                     let current_count = TOOL_COUNT.load(Ordering::SeqCst);
 72 |                     if tool_num < 1 || tool_num > current_count {
 73 |                         return Ok(CallToolResult {
 74 |                             content: vec![Content {
 75 |                                 text: Some(format!(
 76 |                                     "Tool {} does not exist. Only tools 1 through {} have been created.",
 77 |                                     tool_num, current_count
 78 |                                 )),
 79 |                                 r#type: ContentType::Text,
 80 |                                 ..Default::default()
 81 |                             }],
 82 |                             is_error: Some(true),
 83 |                         });
 84 |                     }
 85 |                 } else {
 86 |                     return Ok(CallToolResult {
 87 |                         content: vec![Content {
 88 |                             text: Some(format!("Invalid tool number: {}", tool_number)),
 89 |                             r#type: ContentType::Text,
 90 |                             ..Default::default()
 91 |                         }],
 92 |                         is_error: Some(true),
 93 |                     });
 94 |                 }
 95 |             }
 96 | 
 97 |             Ok(CallToolResult {
 98 |                 content: vec![Content {
 99 |                     text: Some(
100 |                         json!({
101 |                             "message": format!("Called dynamically created tool: {}", tool_name),
102 |                             "tool_number": tool_number,
103 |                         })
104 |                         .to_string(),
105 |                     ),
106 |                     r#type: ContentType::Text,
107 |                     ..Default::default()
108 |                 }],
109 |                 is_error: Some(false),
110 |             })
111 |         }
112 |         _ => Err(Error::new(CustomError(format!(
113 |             "Unknown tool: {}",
114 |             tool_name
115 |         )))),
116 |     }
117 | }
118 | 
119 | pub(crate) fn describe() -> Result<types::ListToolsResult, Error> {
120 |     let current_count = TOOL_COUNT.load(Ordering::SeqCst);
121 |     let mut tools = vec![];
122 | 
123 |     // Always include the add_tool
124 |     tools.push(ToolDescription {
125 |         name: "add_tool".into(),
126 |         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(),
127 |         input_schema: json!({
128 |             "type": "object",
129 |             "properties": {},
130 |             "additionalProperties": false
131 |         })
132 |         .as_object()
133 |         .unwrap()
134 |         .clone(),
135 |     });
136 | 
137 |     // Add all the dynamically created tools
138 |     for i in 1..=current_count {
139 |         tools.push(ToolDescription {
140 |             name: format!("tool_{}", i),
141 |             description: format!(
142 |                 "Dynamically created tool number {}. This tool was added by calling 'add_tool'.",
143 |                 i
144 |             ),
145 |             input_schema: json!({
146 |                 "type": "object",
147 |                 "properties": {},
148 |                 "additionalProperties": false
149 |             })
150 |             .as_object()
151 |             .unwrap()
152 |             .clone(),
153 |         });
154 |     }
155 | 
156 |     Ok(ListToolsResult { tools })
157 | }
158 | 
```

--------------------------------------------------------------------------------
/DEPLOYMENT.md:
--------------------------------------------------------------------------------

```markdown
  1 | Deployment
  2 | ==========
  3 | 
  4 | ## Docker
  5 | 
  6 | Assume you have Docker installed.
  7 | 
  8 | Pull the image
  9 | 
 10 | ```sh
 11 | docker pull ghcr.io/tuananh/hyper-mcp:latest
 12 | ```
 13 | 
 14 | Create a sample config file like this, assume at `/home/ubuntu/config.json`
 15 | 
 16 | ```json
 17 | {
 18 |   "plugins": {
 19 |     "time": {
 20 |       "url": "oci://ghcr.io/tuananh/time-plugin:latest"
 21 |     },
 22 |     "qr_code": {
 23 |       "url": "oci://ghcr.io/tuananh/qrcode-plugin:latest"
 24 |     }
 25 |   }
 26 | }
 27 | ```
 28 | 
 29 | > 📖 **For authentication configuration and advanced options, see [RUNTIME_CONFIG.md](./RUNTIME_CONFIG.md)**
 30 | 
 31 | ### Authentication in Docker
 32 | 
 33 | For production deployments with authentication, you have several options:
 34 | 
 35 | **Option 1: Mount keyring (Linux only)**
 36 | ```sh
 37 | docker run -d \
 38 |     --name hyper-mcp \
 39 |     -p 3001:3001 \
 40 |     -v /home/ubuntu/config.json:/app/config.json \
 41 |     -v ~/.local/share/keyrings:/home/appuser/.local/share/keyrings:ro \
 42 |     ghcr.io/tuananh/hyper-mcp \
 43 |     --transport sse \
 44 |     --bind-address 0.0.0.0:3001 \
 45 |     --config-file /app/config.json
 46 | ```
 47 | 
 48 | **Option 2: Use Docker secrets**
 49 | ```sh
 50 | # Create secrets
 51 | echo '{"type":"basic","username":"user","password":"pass"}' | docker secret create registry_auth -
 52 | 
 53 | # Run with secrets
 54 | docker run -d \
 55 |     --name hyper-mcp \
 56 |     -p 3001:3001 \
 57 |     -v /home/ubuntu/config.json:/app/config.json \
 58 |     --secret registry_auth \
 59 |     ghcr.io/tuananh/hyper-mcp \
 60 |     --transport sse \
 61 |     --bind-address 0.0.0.0:3001 \
 62 |     --config-file /app/config.json
 63 | ```
 64 | 
 65 | **Option 3: Environment-based credentials**
 66 | ```sh
 67 | docker run -d \
 68 |     --name hyper-mcp \
 69 |     -p 3001:3001 \
 70 |     -v /home/ubuntu/config.json:/app/config.json \
 71 |     -e REGISTRY_USER="username" \
 72 |     -e REGISTRY_PASS="password" \
 73 |     ghcr.io/tuananh/hyper-mcp \
 74 |     --transport sse \
 75 |     --bind-address 0.0.0.0:3001 \
 76 |     --config-file /app/config.json
 77 | ```
 78 | 
 79 | Run the container
 80 | 
 81 | ```sh
 82 | docker run -d \
 83 |     --name hyper-mcp \
 84 |     -p 3001:3001 \
 85 |     -v /home/ubuntu/config.json:/app/config.json \
 86 |     ghcr.io/tuananh/hyper-mcp \
 87 |     --transport sse \
 88 |     --bind-address 0.0.0.0:3001 \
 89 |     --config-file /app/config.json
 90 | ```
 91 | 
 92 | Note that we need to bind to `--bind-address 0.0.0.0:3001` in order to access from the host.
 93 | 
 94 | ## GCP Cloud Run
 95 | 
 96 | ### Prerequisites
 97 | - Google Cloud SDK installed
 98 | - Terraform installed
 99 | - A GCP project with Cloud Run and Secret Manager APIs enabled
100 | 
101 | ### Configuration
102 | 
103 | 1. Create a `terraform.tfvars` file with your configuration in `iac` folder:
104 | 
105 | ```hcl
106 | name       = "hyper-mcp"
107 | project_id = "your-project-id"
108 | region     = "asia-southeast1"  # or your preferred region
109 | ```
110 | 
111 | 2. Create a config file in Secret Manager:
112 | 
113 | The config file will be automatically created and managed by Terraform. Here's an example of what it contains:
114 | 
115 | ```json
116 | {
117 |   "plugins": {
118 |     "time": {
119 |       "url": "oci://ghcr.io/tuananh/time-plugin:latest"
120 |     },
121 |     "qr_code": {
122 |       "url": "oci://ghcr.io/tuananh/qrcode-plugin:latest"
123 |     }
124 |   }
125 | }
126 | ```
127 | 
128 | For production deployments with authentication, update the config to use Secret Manager:
129 | 
130 | ```json
131 | {
132 |   "auths": {
133 |     "https://private.registry.example.com": {
134 |       "type": "basic",
135 |       "username": "registry-user",
136 |       "password": "registry-password"
137 |     }
138 |   },
139 |   "plugins": {
140 |     "time": {
141 |       "url": "oci://ghcr.io/tuananh/time-plugin:latest"
142 |     },
143 |     "private_plugin": {
144 |       "url": "https://private.registry.example.com/secure-plugin:latest",
145 |       "runtime_config": {
146 |         "allowed_hosts": ["private.registry.example.com"]
147 |       }
148 |     }
149 |   }
150 | }
151 | ```
152 | 
153 | 3. Deploy using Terraform:
154 | 
155 | ```sh
156 | cd iac
157 | terraform init
158 | terraform plan
159 | terraform apply
160 | ```
161 | 
162 | The service will be deployed with:
163 | - Port 3001 exposed
164 | - Config file mounted at `/app/config.json`
165 | - Public access enabled
166 | - SSE transport mode
167 | - Bound to 0.0.0.0:3001
168 | 
169 | ### Accessing the Service
170 | 
171 | After deployment, you can get the service URL using:
172 | 
173 | ```sh
174 | terraform output url
175 | ```
176 | 
177 | The service will be accessible at the provided URL.
178 | 
179 | ### Authentication with GCP Secret Manager
180 | 
181 | For secure credential management in GCP Cloud Run:
182 | 
183 | 1. Store authentication credentials in Secret Manager:
184 | ```sh
185 | # Store registry credentials
186 | gcloud secrets create registry-auth --data-file=- <<< '{"type":"basic","username":"user","password":"pass"}'
187 | 
188 | # Store API tokens
189 | gcloud secrets create api-token --data-file=- <<< '{"type":"token","token":"your-api-token"}'
190 | ```
191 | 
192 | 2. Update your Terraform configuration to mount secrets:
193 | ```hcl
194 | resource "google_cloud_run_service" "hyper_mcp" {
195 |   # ... existing configuration ...
196 | 
197 |   template {
198 |     spec {
199 |       containers {
200 |         # ... existing container config ...
201 | 
202 |         env {
203 |           name = "CONFIG_FILE"
204 |           value = "/app/config.json"
205 |         }
206 | 
207 |         volume_mounts {
208 |           name       = "secrets"
209 |           mount_path = "/app/secrets"
210 |         }
211 |       }
212 | 
213 |       volumes {
214 |         name = "secrets"
215 |         secret {
216 |           secret_name = google_secret_manager_secret.registry_auth.secret_id
217 |         }
218 |       }
219 |     }
220 |   }
221 | }
222 | ```
223 | 
224 | ## Production Security Considerations
225 | 
226 | ### Authentication Best Practices
227 | - **Never include credentials in Docker images or version control**
228 | - **Use keyring authentication for local development**
229 | - **Use cloud-native secret management for production** (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault)
230 | - **Rotate credentials regularly and update keyring/secret stores**
231 | - **Use least-privilege access principles** for service accounts
232 | - **Monitor authentication failures** in logs
233 | 
234 | ### Container Security
235 | - **Run containers with non-root users**
236 | - **Use read-only filesystems where possible**
237 | - **Limit container network access**
238 | - **Scan images for vulnerabilities regularly**
239 | - **Use distroless or minimal base images**
240 | 
241 | ## Cloudflare Workers
242 | 
243 | Not possible yet but it's in my TODO list.
244 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/crypto-price/pdk.gen.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"errors"
  5 | 
  6 | 	pdk "github.com/extism/go-pdk"
  7 | )
  8 | 
  9 | //export call
 10 | func _Call() int32 {
 11 | 	var err error
 12 | 	_ = err
 13 | 	pdk.Log(pdk.LogDebug, "Call: getting JSON input")
 14 | 	var input CallToolRequest
 15 | 	err = pdk.InputJSON(&input)
 16 | 	if err != nil {
 17 | 		pdk.SetError(err)
 18 | 		return -1
 19 | 	}
 20 | 
 21 | 	pdk.Log(pdk.LogDebug, "Call: calling implementation function")
 22 | 	output, err := Call(input)
 23 | 	if err != nil {
 24 | 		pdk.SetError(err)
 25 | 		return -1
 26 | 	}
 27 | 
 28 | 	pdk.Log(pdk.LogDebug, "Call: setting JSON output")
 29 | 	err = pdk.OutputJSON(output)
 30 | 	if err != nil {
 31 | 		pdk.SetError(err)
 32 | 		return -1
 33 | 	}
 34 | 
 35 | 	pdk.Log(pdk.LogDebug, "Call: returning")
 36 | 	return 0
 37 | }
 38 | 
 39 | //export describe
 40 | func _Describe() int32 {
 41 | 	var err error
 42 | 	_ = err
 43 | 	output, err := Describe()
 44 | 	if err != nil {
 45 | 		pdk.SetError(err)
 46 | 		return -1
 47 | 	}
 48 | 
 49 | 	pdk.Log(pdk.LogDebug, "Describe: setting JSON output")
 50 | 	err = pdk.OutputJSON(output)
 51 | 	if err != nil {
 52 | 		pdk.SetError(err)
 53 | 		return -1
 54 | 	}
 55 | 
 56 | 	pdk.Log(pdk.LogDebug, "Describe: returning")
 57 | 	return 0
 58 | }
 59 | 
 60 | type BlobResourceContents struct {
 61 | 	// A base64-encoded string representing the binary data of the item.
 62 | 	Blob string `json:"blob"`
 63 | 	// The MIME type of this resource, if known.
 64 | 	MimeType *string `json:"mimeType,omitempty"`
 65 | 	// The URI of this resource.
 66 | 	Uri string `json:"uri"`
 67 | }
 68 | 
 69 | // Used by the client to invoke a tool provided by the server.
 70 | type CallToolRequest struct {
 71 | 	Method *string `json:"method,omitempty"`
 72 | 	Params Params  `json:"params"`
 73 | }
 74 | 
 75 | // The server's response to a tool call.
 76 | //
 77 | // Any errors that originate from the tool SHOULD be reported inside the result
 78 | // object, with `isError` set to true, _not_ as an MCP protocol-level error
 79 | // response. Otherwise, the LLM would not be able to see that an error occurred
 80 | // and self-correct.
 81 | //
 82 | // However, any errors in _finding_ the tool, an error indicating that the
 83 | // server does not support tool calls, or any other exceptional conditions,
 84 | // should be reported as an MCP error response.
 85 | type CallToolResult struct {
 86 | 	Content []Content `json:"content"`
 87 | 	// Whether the tool call ended in an error.
 88 | 	//
 89 | 	// If not set, this is assumed to be false (the call was successful).
 90 | 	IsError *bool `json:"isError,omitempty"`
 91 | }
 92 | 
 93 | // A content response.
 94 | // For text content set type to ContentType.Text and set the `text` property
 95 | // For image content set type to ContentType.Image and set the `data` and `mimeType` properties
 96 | type Content struct {
 97 | 	Annotations *TextAnnotation `json:"annotations,omitempty"`
 98 | 	// The base64-encoded image data.
 99 | 	Data *string `json:"data,omitempty"`
100 | 	// The MIME type of the image. Different providers may support different image types.
101 | 	MimeType *string `json:"mimeType,omitempty"`
102 | 	// The text content of the message.
103 | 	Text *string     `json:"text,omitempty"`
104 | 	Type ContentType `json:"type"`
105 | }
106 | 
107 | type ContentType string
108 | 
109 | const (
110 | 	ContentTypeText     ContentType = "text"
111 | 	ContentTypeImage    ContentType = "image"
112 | 	ContentTypeResource ContentType = "resource"
113 | )
114 | 
115 | func (v ContentType) String() string {
116 | 	switch v {
117 | 	case ContentTypeText:
118 | 		return `text`
119 | 	case ContentTypeImage:
120 | 		return `image`
121 | 	case ContentTypeResource:
122 | 		return `resource`
123 | 	default:
124 | 		return ""
125 | 	}
126 | }
127 | 
128 | func stringToContentType(s string) (ContentType, error) {
129 | 	switch s {
130 | 	case `text`:
131 | 		return ContentTypeText, nil
132 | 	case `image`:
133 | 		return ContentTypeImage, nil
134 | 	case `resource`:
135 | 		return ContentTypeResource, nil
136 | 	default:
137 | 		return ContentType(""), errors.New("unable to convert string to ContentType")
138 | 	}
139 | }
140 | 
141 | // Provides one or more descriptions of the tools available in this servlet.
142 | type ListToolsResult struct {
143 | 	// The list of ToolDescription objects provided by this servlet.
144 | 	Tools []ToolDescription `json:"tools"`
145 | }
146 | 
147 | type Params struct {
148 | 	Arguments interface{} `json:"arguments,omitempty"`
149 | 	Name      string      `json:"name"`
150 | }
151 | 
152 | // The sender or recipient of messages and data in a conversation.
153 | type Role string
154 | 
155 | const (
156 | 	RoleAssistant Role = "assistant"
157 | 	RoleUser      Role = "user"
158 | )
159 | 
160 | func (v Role) String() string {
161 | 	switch v {
162 | 	case RoleAssistant:
163 | 		return `assistant`
164 | 	case RoleUser:
165 | 		return `user`
166 | 	default:
167 | 		return ""
168 | 	}
169 | }
170 | 
171 | func stringToRole(s string) (Role, error) {
172 | 	switch s {
173 | 	case `assistant`:
174 | 		return RoleAssistant, nil
175 | 	case `user`:
176 | 		return RoleUser, nil
177 | 	default:
178 | 		return Role(""), errors.New("unable to convert string to Role")
179 | 	}
180 | }
181 | 
182 | // A text annotation
183 | type TextAnnotation struct {
184 | 	// Describes who the intended customer of this object or data is.
185 | 	//
186 | 	// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
187 | 	Audience []Role `json:"audience,omitempty"`
188 | 	// Describes how important this data is for operating the server.
189 | 	//
190 | 	// A value of 1 means "most important," and indicates that the data is
191 | 	// effectively required, while 0 means "least important," and indicates that
192 | 	// the data is entirely optional.
193 | 	Priority float32 `json:"priority,omitempty"`
194 | }
195 | 
196 | type TextResourceContents struct {
197 | 	// The MIME type of this resource, if known.
198 | 	MimeType *string `json:"mimeType,omitempty"`
199 | 	// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
200 | 	Text string `json:"text"`
201 | 	// The URI of this resource.
202 | 	Uri string `json:"uri"`
203 | }
204 | 
205 | // Describes the capabilities and expected paramters of the tool function
206 | type ToolDescription struct {
207 | 	// A description of the tool
208 | 	Description string `json:"description"`
209 | 	// The JSON schema describing the argument input
210 | 	InputSchema interface{} `json:"inputSchema"`
211 | 	// The name of the tool. It should match the plugin / binding name.
212 | 	Name string `json:"name"`
213 | }
214 | 
215 | // Note: leave this in place, as the Go compiler will find the `export` function as the entrypoint.
216 | func main() {}
217 | 
```

--------------------------------------------------------------------------------
/examples/plugins/v1/github/pdk.gen.go:
--------------------------------------------------------------------------------

```go
  1 | // THIS FILE WAS GENERATED BY `xtp-go-bindgen`. DO NOT EDIT.
  2 | package main
  3 | 
  4 | import (
  5 | 	"errors"
  6 | 
  7 | 	pdk "github.com/extism/go-pdk"
  8 | )
  9 | 
 10 | //export call
 11 | func _Call() int32 {
 12 | 	var err error
 13 | 	_ = err
 14 | 	pdk.Log(pdk.LogDebug, "Call: getting JSON input")
 15 | 	var input CallToolRequest
 16 | 	err = pdk.InputJSON(&input)
 17 | 	if err != nil {
 18 | 		pdk.SetError(err)
 19 | 		return -1
 20 | 	}
 21 | 
 22 | 	pdk.Log(pdk.LogDebug, "Call: calling implementation function")
 23 | 	output, err := Call(input)
 24 | 	if err != nil {
 25 | 		pdk.SetError(err)
 26 | 		return -1
 27 | 	}
 28 | 
 29 | 	pdk.Log(pdk.LogDebug, "Call: setting JSON output")
 30 | 	err = pdk.OutputJSON(output)
 31 | 	if err != nil {
 32 | 		pdk.SetError(err)
 33 | 		return -1
 34 | 	}
 35 | 
 36 | 	pdk.Log(pdk.LogDebug, "Call: returning")
 37 | 	return 0
 38 | }
 39 | 
 40 | //export describe
 41 | func _Describe() int32 {
 42 | 	var err error
 43 | 	_ = err
 44 | 	output, err := Describe()
 45 | 	if err != nil {
 46 | 		pdk.SetError(err)
 47 | 		return -1
 48 | 	}
 49 | 
 50 | 	pdk.Log(pdk.LogDebug, "Describe: setting JSON output")
 51 | 	err = pdk.OutputJSON(output)
 52 | 	if err != nil {
 53 | 		pdk.SetError(err)
 54 | 		return -1
 55 | 	}
 56 | 
 57 | 	pdk.Log(pdk.LogDebug, "Describe: returning")
 58 | 	return 0
 59 | }
 60 | 
 61 | //
 62 | type BlobResourceContents struct {
 63 | 	// A base64-encoded string representing the binary data of the item.
 64 | 	Blob string `json:"blob"`
 65 | 	// The MIME type of this resource, if known.
 66 | 	MimeType *string `json:"mimeType,omitempty"`
 67 | 	// The URI of this resource.
 68 | 	Uri string `json:"uri"`
 69 | }
 70 | 
 71 | // Used by the client to invoke a tool provided by the server.
 72 | type CallToolRequest struct {
 73 | 	Method *string `json:"method,omitempty"`
 74 | 	Params Params  `json:"params"`
 75 | }
 76 | 
 77 | // The server's response to a tool call.
 78 | //
 79 | // Any errors that originate from the tool SHOULD be reported inside the result
 80 | // object, with `isError` set to true, _not_ as an MCP protocol-level error
 81 | // response. Otherwise, the LLM would not be able to see that an error occurred
 82 | // and self-correct.
 83 | //
 84 | // However, any errors in _finding_ the tool, an error indicating that the
 85 | // server does not support tool calls, or any other exceptional conditions,
 86 | // should be reported as an MCP error response.
 87 | type CallToolResult struct {
 88 | 	Content []Content `json:"content"`
 89 | 	// Whether the tool call ended in an error.
 90 | 	//
 91 | 	// If not set, this is assumed to be false (the call was successful).
 92 | 	IsError *bool `json:"isError,omitempty"`
 93 | }
 94 | 
 95 | // A content response.
 96 | // For text content set type to ContentType.Text and set the `text` property
 97 | // For image content set type to ContentType.Image and set the `data` and `mimeType` properties
 98 | type Content struct {
 99 | 	Annotations *TextAnnotation `json:"annotations,omitempty"`
100 | 	// The base64-encoded image data.
101 | 	Data *string `json:"data,omitempty"`
102 | 	// The MIME type of the image. Different providers may support different image types.
103 | 	MimeType *string `json:"mimeType,omitempty"`
104 | 	// The text content of the message.
105 | 	Text *string     `json:"text,omitempty"`
106 | 	Type ContentType `json:"type"`
107 | }
108 | 
109 | //
110 | type ContentType string
111 | 
112 | const (
113 | 	ContentTypeText     ContentType = "text"
114 | 	ContentTypeImage    ContentType = "image"
115 | 	ContentTypeResource ContentType = "resource"
116 | )
117 | 
118 | func (v ContentType) String() string {
119 | 	switch v {
120 | 	case ContentTypeText:
121 | 		return `text`
122 | 	case ContentTypeImage:
123 | 		return `image`
124 | 	case ContentTypeResource:
125 | 		return `resource`
126 | 	default:
127 | 		return ""
128 | 	}
129 | }
130 | 
131 | func stringToContentType(s string) (ContentType, error) {
132 | 	switch s {
133 | 	case `text`:
134 | 		return ContentTypeText, nil
135 | 	case `image`:
136 | 		return ContentTypeImage, nil
137 | 	case `resource`:
138 | 		return ContentTypeResource, nil
139 | 	default:
140 | 		return ContentType(""), errors.New("unable to convert string to ContentType")
141 | 	}
142 | }
143 | 
144 | // Provides one or more descriptions of the tools available in this servlet.
145 | type ListToolsResult struct {
146 | 	// The list of ToolDescription objects provided by this servlet.
147 | 	Tools []ToolDescription `json:"tools"`
148 | }
149 | 
150 | //
151 | type Params struct {
152 | 	Arguments interface{} `json:"arguments,omitempty"`
153 | 	Name      string      `json:"name"`
154 | }
155 | 
156 | // The sender or recipient of messages and data in a conversation.
157 | type Role string
158 | 
159 | const (
160 | 	RoleAssistant Role = "assistant"
161 | 	RoleUser      Role = "user"
162 | )
163 | 
164 | func (v Role) String() string {
165 | 	switch v {
166 | 	case RoleAssistant:
167 | 		return `assistant`
168 | 	case RoleUser:
169 | 		return `user`
170 | 	default:
171 | 		return ""
172 | 	}
173 | }
174 | 
175 | func stringToRole(s string) (Role, error) {
176 | 	switch s {
177 | 	case `assistant`:
178 | 		return RoleAssistant, nil
179 | 	case `user`:
180 | 		return RoleUser, nil
181 | 	default:
182 | 		return Role(""), errors.New("unable to convert string to Role")
183 | 	}
184 | }
185 | 
186 | // A text annotation
187 | type TextAnnotation struct {
188 | 	// Describes who the intended customer of this object or data is.
189 | 	//
190 | 	// It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`).
191 | 	Audience []Role `json:"audience,omitempty"`
192 | 	// Describes how important this data is for operating the server.
193 | 	//
194 | 	// A value of 1 means "most important," and indicates that the data is
195 | 	// effectively required, while 0 means "least important," and indicates that
196 | 	// the data is entirely optional.
197 | 	Priority float32 `json:"priority,omitempty"`
198 | }
199 | 
200 | //
201 | type TextResourceContents struct {
202 | 	// The MIME type of this resource, if known.
203 | 	MimeType *string `json:"mimeType,omitempty"`
204 | 	// The text of the item. This must only be set if the item can actually be represented as text (not binary data).
205 | 	Text string `json:"text"`
206 | 	// The URI of this resource.
207 | 	Uri string `json:"uri"`
208 | }
209 | 
210 | // Describes the capabilities and expected paramters of the tool function
211 | type ToolDescription struct {
212 | 	// A description of the tool
213 | 	Description string `json:"description"`
214 | 	// The JSON schema describing the argument input
215 | 	InputSchema interface{} `json:"inputSchema"`
216 | 	// The name of the tool. It should match the plugin / binding name.
217 | 	Name string `json:"name"`
218 | }
219 | 
220 | // Note: leave this in place, as the Go compiler will find the `export` function as the entrypoint.
221 | func main() {}
222 | 
```

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

```rust
  1 | mod pdk;
  2 | 
  3 | use extism_pdk::*;
  4 | use pdk::types::{
  5 |     CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
  6 | };
  7 | use rusqlite::{Connection, params};
  8 | use serde::{Deserialize, Serialize};
  9 | use serde_json::json;
 10 | use std::sync::Once;
 11 | use uuid::Uuid;
 12 | 
 13 | static DB_INIT: Once = Once::new();
 14 | 
 15 | #[derive(Debug, Serialize, Deserialize)]
 16 | struct Memory {
 17 |     id: String,
 18 |     content: String,
 19 | }
 20 | 
 21 | fn init_db(db_path: &str) -> Result<(), Error> {
 22 |     let conn = Connection::open_with_flags(
 23 |         db_path,
 24 |         rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
 25 |     )?;
 26 | 
 27 |     conn.execute(
 28 |         "CREATE TABLE IF NOT EXISTS memories (
 29 |             id TEXT PRIMARY KEY,
 30 |             content TEXT NOT NULL,
 31 |             created_at INTEGER DEFAULT (strftime('%s', 'now'))
 32 |         )",
 33 |         [],
 34 |     )?;
 35 | 
 36 |     Ok(())
 37 | }
 38 | 
 39 | fn get_db_path() -> Result<String, Error> {
 40 |     config::get("db_path")?
 41 |         .ok_or_else(|| Error::msg("db_path configuration is required but not set"))
 42 | }
 43 | 
 44 | fn store_memory(content: &str, db_path: &str) -> Result<String, Error> {
 45 |     let conn = Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE)?;
 46 |     let id = Uuid::new_v4().to_string();
 47 | 
 48 |     conn.execute(
 49 |         "INSERT INTO memories (id, content) VALUES (?, ?)",
 50 |         params![id, content],
 51 |     )?;
 52 | 
 53 |     Ok(id)
 54 | }
 55 | 
 56 | fn get_memory(id: &str, db_path: &str) -> Result<Option<Memory>, Error> {
 57 |     let conn = Connection::open_with_flags(db_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE)?;
 58 | 
 59 |     let mut stmt = conn.prepare("SELECT id, content FROM memories WHERE id = ?")?;
 60 |     let mut rows = stmt.query(params![id])?;
 61 | 
 62 |     if let Some(row) = rows.next()? {
 63 |         Ok(Some(Memory {
 64 |             id: row.get(0)?,
 65 |             content: row.get(1)?,
 66 |         }))
 67 |     } else {
 68 |         Ok(None)
 69 |     }
 70 | }
 71 | 
 72 | pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
 73 |     let db_path = get_db_path()?;
 74 |     DB_INIT.call_once(|| {
 75 |         init_db(&db_path).expect("Failed to initialize database");
 76 |     });
 77 | 
 78 |     match input.params.name.as_str() {
 79 |         "store_memory" => {
 80 |             let args = input.params.arguments.unwrap_or_default();
 81 |             let content = match args.get("content") {
 82 |                 Some(v) if v.is_string() => v.as_str().unwrap(),
 83 |                 _ => return Err(Error::msg("content parameter is required")),
 84 |             };
 85 | 
 86 |             let id = store_memory(content, &db_path)?;
 87 | 
 88 |             Ok(CallToolResult {
 89 |                 is_error: None,
 90 |                 content: vec![Content {
 91 |                     annotations: None,
 92 |                     text: Some(json!({ "id": id }).to_string()),
 93 |                     mime_type: Some("application/json".to_string()),
 94 |                     r#type: ContentType::Text,
 95 |                     data: None,
 96 |                 }],
 97 |             })
 98 |         }
 99 |         "get_memory" => {
100 |             let args = input.params.arguments.unwrap_or_default();
101 |             let id = match args.get("id") {
102 |                 Some(v) if v.is_string() => v.as_str().unwrap(),
103 |                 _ => return Err(Error::msg("id parameter is required")),
104 |             };
105 | 
106 |             match get_memory(id, &db_path)? {
107 |                 Some(memory) => Ok(CallToolResult {
108 |                     is_error: None,
109 |                     content: vec![Content {
110 |                         annotations: None,
111 |                         text: Some(serde_json::to_string(&memory)?),
112 |                         mime_type: Some("application/json".to_string()),
113 |                         r#type: ContentType::Text,
114 |                         data: None,
115 |                     }],
116 |                 }),
117 |                 None => Ok(CallToolResult {
118 |                     is_error: Some(true),
119 |                     content: vec![Content {
120 |                         annotations: None,
121 |                         text: Some("Memory not found".to_string()),
122 |                         mime_type: None,
123 |                         r#type: ContentType::Text,
124 |                         data: None,
125 |                     }],
126 |                 }),
127 |             }
128 |         }
129 |         _ => Ok(CallToolResult {
130 |             is_error: Some(true),
131 |             content: vec![Content {
132 |                 annotations: None,
133 |                 text: Some(format!("Unknown tool: {}", input.params.name)),
134 |                 mime_type: None,
135 |                 r#type: ContentType::Text,
136 |                 data: None,
137 |             }],
138 |         }),
139 |     }
140 | }
141 | 
142 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
143 |     Ok(ListToolsResult {
144 |         tools: vec![
145 |             ToolDescription {
146 |                 name: "store_memory".into(),
147 |                 description: "Store content in memory and return a unique ID".into(),
148 |                 input_schema: json!({
149 |                     "type": "object",
150 |                     "properties": {
151 |                         "content": {
152 |                             "type": "string",
153 |                             "description": "The content to store",
154 |                         }
155 |                     },
156 |                     "required": ["content"],
157 |                 })
158 |                 .as_object()
159 |                 .unwrap()
160 |                 .clone(),
161 |             },
162 |             ToolDescription {
163 |                 name: "get_memory".into(),
164 |                 description: "Retrieve content from memory by ID".into(),
165 |                 input_schema: json!({
166 |                     "type": "object",
167 |                     "properties": {
168 |                         "id": {
169 |                             "type": "string",
170 |                             "description": "The ID of the content to retrieve",
171 |                         }
172 |                     },
173 |                     "required": ["id"],
174 |                 })
175 |                 .as_object()
176 |                 .unwrap()
177 |                 .clone(),
178 |             },
179 |         ],
180 |     })
181 | }
182 | 
```

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

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"encoding/json"
  5 | 	"fmt"
  6 | 
  7 | 	"github.com/extism/go-pdk"
  8 | )
  9 | 
 10 | var (
 11 | 	CreateGistTool = ToolDescription{
 12 | 		Name:        "gh-create-gist",
 13 | 		Description: "Create a GitHub Gist",
 14 | 		InputSchema: schema{
 15 | 			"type": "object",
 16 | 			"properties": props{
 17 | 				"description": prop("string", "Description of the gist"),
 18 | 				"files": SchemaProperty{
 19 | 					Type:        "object",
 20 | 					Description: "Files contained in the gist.",
 21 | 					AdditionalProperties: &schema{
 22 | 						"type": "object",
 23 | 						"properties": schema{
 24 | 							"content": schema{
 25 | 								"type":        "string",
 26 | 								"description": "Content of the file",
 27 | 							},
 28 | 						},
 29 | 						"required": []string{"content"},
 30 | 					},
 31 | 				},
 32 | 			},
 33 | 			"required": []string{"files"},
 34 | 		},
 35 | 	}
 36 | 	GetGistTool = ToolDescription{
 37 | 		Name:        "gh-get-gist",
 38 | 		Description: "Gets a specified gist.",
 39 | 		InputSchema: schema{
 40 | 			"type": "object",
 41 | 			"properties": props{
 42 | 				"gist_id": prop("string", "The unique identifier of the gist."),
 43 | 			},
 44 | 			"required": []string{"gist_id"},
 45 | 		},
 46 | 	}
 47 | 	UpdateGistTool = ToolDescription{
 48 | 		Name:        "gh-update-gist",
 49 | 		Description: "Lists pull requests in a specified repository. Supports different response formats via accept parameter.",
 50 | 		InputSchema: schema{
 51 | 			"type": "object",
 52 | 			"properties": props{
 53 | 				"gist_id":     prop("string", "The unique identifier of the gist."),
 54 | 				"description": prop("string", "Description of the gist"),
 55 | 				"files": SchemaProperty{
 56 | 					Type:        "object",
 57 | 					Description: "Files contained in the gist.",
 58 | 					AdditionalProperties: &schema{
 59 | 						"type": "object",
 60 | 						"properties": schema{
 61 | 							"content": schema{
 62 | 								"type":        "string",
 63 | 								"description": "Content of the file",
 64 | 							},
 65 | 						},
 66 | 						"required": []string{"content"},
 67 | 					},
 68 | 				},
 69 | 			},
 70 | 			"required": []string{"gist_id"},
 71 | 		},
 72 | 	}
 73 | 	DeleteGistTool = ToolDescription{
 74 | 		Name:        "gh-delete-gist",
 75 | 		Description: "Delete a specified gist.",
 76 | 		InputSchema: schema{
 77 | 			"type": "object",
 78 | 			"properties": props{
 79 | 				"gist_id": prop("string", "The unique identifier of the gist."),
 80 | 			},
 81 | 			"required": []string{"gist_id"},
 82 | 		},
 83 | 	}
 84 | )
 85 | 
 86 | var GistTools = []ToolDescription{
 87 | 	CreateGistTool,
 88 | 	GetGistTool,
 89 | 	UpdateGistTool,
 90 | 	DeleteGistTool,
 91 | }
 92 | 
 93 | func gistCreate(apiKey, description string, files map[string]any) CallToolResult {
 94 | 	url := "https://api.github.com/gists"
 95 | 	req := pdk.NewHTTPRequest(pdk.MethodPost, url)
 96 | 	req.SetHeader("Authorization", fmt.Sprintf("token %s", apiKey))
 97 | 	req.SetHeader("Content-Type", "application/json")
 98 | 	req.SetHeader("Accept", "application/vnd.github+json")
 99 | 	req.SetHeader("User-Agent", "github-mcpx-servlet")
100 | 
101 | 	data := map[string]any{
102 | 		"description": description,
103 | 		"files":       files,
104 | 	}
105 | 	res, err := json.Marshal(data)
106 | 	if err != nil {
107 | 		return CallToolResult{
108 | 			IsError: some(true),
109 | 			Content: []Content{{
110 | 				Type: ContentTypeText,
111 | 				Text: some(fmt.Sprintf("Failed to marshal gist data: %s", err)),
112 | 			}},
113 | 		}
114 | 	}
115 | 	req.SetBody(res)
116 | 	resp := req.Send()
117 | 	if resp.Status() != 201 {
118 | 		return CallToolResult{
119 | 			IsError: some(true),
120 | 			Content: []Content{{
121 | 				Type: ContentTypeText,
122 | 				Text: some(fmt.Sprintf("Failed to create gist: %d %s", resp.Status(), string(resp.Body()))),
123 | 			}},
124 | 		}
125 | 	}
126 | 
127 | 	return CallToolResult{
128 | 		Content: []Content{{
129 | 			Type: ContentTypeText,
130 | 			Text: some(string(resp.Body())),
131 | 		}},
132 | 	}
133 | }
134 | 
135 | func gistUpdate(apiKey, gistId, description string, files map[string]any) CallToolResult {
136 | 	url := fmt.Sprintf("https://api.github.com/gists/%s", gistId)
137 | 	req := pdk.NewHTTPRequest(pdk.MethodPatch, url)
138 | 	req.SetHeader("Authorization", fmt.Sprintf("token %s", apiKey))
139 | 	req.SetHeader("Content-Type", "application/json")
140 | 	req.SetHeader("Accept", "application/vnd.github+json")
141 | 	req.SetHeader("User-Agent", "github-mcpx-servlet")
142 | 
143 | 	data := map[string]any{
144 | 		"description": description,
145 | 		"files":       files,
146 | 	}
147 | 	res, err := json.Marshal(data)
148 | 	if err != nil {
149 | 		return CallToolResult{
150 | 			IsError: some(true),
151 | 			Content: []Content{{
152 | 				Type: ContentTypeText,
153 | 				Text: some(fmt.Sprintf("Failed to marshal gist data: %s", err)),
154 | 			}},
155 | 		}
156 | 	}
157 | 	req.SetBody(res)
158 | 	resp := req.Send()
159 | 	if resp.Status() != 201 {
160 | 		return CallToolResult{
161 | 			IsError: some(true),
162 | 			Content: []Content{{
163 | 				Type: ContentTypeText,
164 | 				Text: some(fmt.Sprintf("Failed to create gist: %d %s", resp.Status(), string(resp.Body()))),
165 | 			}},
166 | 		}
167 | 	}
168 | 
169 | 	return CallToolResult{
170 | 		Content: []Content{{
171 | 			Type: ContentTypeText,
172 | 			Text: some(string(resp.Body())),
173 | 		}},
174 | 	}
175 | }
176 | 
177 | func gistGet(apiKey, gistId string) CallToolResult {
178 | 	url := fmt.Sprintf("https://api.github.com/gists/%s", gistId)
179 | 	req := pdk.NewHTTPRequest(pdk.MethodGet, url)
180 | 	req.SetHeader("Authorization", fmt.Sprintf("token %s", apiKey))
181 | 	req.SetHeader("Content-Type", "application/json")
182 | 	req.SetHeader("Accept", "application/vnd.github+json")
183 | 	req.SetHeader("User-Agent", "github-mcpx-servlet")
184 | 
185 | 	resp := req.Send()
186 | 	if resp.Status() != 201 {
187 | 		return CallToolResult{
188 | 			IsError: some(true),
189 | 			Content: []Content{{
190 | 				Type: ContentTypeText,
191 | 				Text: some(fmt.Sprintf("Failed to create branch: %d %s", resp.Status(), string(resp.Body()))),
192 | 			}},
193 | 		}
194 | 	}
195 | 
196 | 	return CallToolResult{
197 | 		Content: []Content{{
198 | 			Type: ContentTypeText,
199 | 			Text: some(string(resp.Body())),
200 | 		}},
201 | 	}
202 | }
203 | 
204 | func gistDelete(apiKey, gistId string) CallToolResult {
205 | 	url := fmt.Sprintf("https://api.github.com/gists/%s", gistId)
206 | 	req := pdk.NewHTTPRequest(pdk.MethodDelete, url)
207 | 	req.SetHeader("Authorization", fmt.Sprintf("token %s", apiKey))
208 | 	req.SetHeader("Content-Type", "application/json")
209 | 	req.SetHeader("Accept", "application/vnd.github+json")
210 | 	req.SetHeader("User-Agent", "github-mcpx-servlet")
211 | 
212 | 	resp := req.Send()
213 | 	if resp.Status() != 201 {
214 | 		return CallToolResult{
215 | 			IsError: some(true),
216 | 			Content: []Content{{
217 | 				Type: ContentTypeText,
218 | 				Text: some(fmt.Sprintf("Failed to create branch: %d %s", resp.Status(), string(resp.Body()))),
219 | 			}},
220 | 		}
221 | 	}
222 | 
223 | 	return CallToolResult{
224 | 		Content: []Content{{
225 | 			Type: ContentTypeText,
226 | 			Text: some(string(resp.Body())),
227 | 		}},
228 | 	}
229 | }
230 | 
```

--------------------------------------------------------------------------------
/templates/plugins/go/exports.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	pdk "github.com/extism/go-pdk"
  5 | )
  6 | 
  7 | //export call_tool
  8 | func _CallTool() int32 {
  9 | 	var err error
 10 | 	_ = err
 11 | 	pdk.Log(pdk.LogDebug, "CallTool: getting JSON input")
 12 | 	var input CallToolRequest
 13 | 	err = pdk.InputJSON(&input)
 14 | 	if err != nil {
 15 | 		pdk.SetError(err)
 16 | 		return -1
 17 | 	}
 18 | 
 19 | 	pdk.Log(pdk.LogDebug, "CallTool: calling implementation function")
 20 | 	output, err := CallTool(input)
 21 | 	if err != nil {
 22 | 		pdk.SetError(err)
 23 | 		return -1
 24 | 	}
 25 | 
 26 | 	if output == nil {
 27 | 		pdk.SetErrorString("CallTool: output is nil")
 28 | 		return -1
 29 | 	}
 30 | 
 31 | 	pdk.Log(pdk.LogDebug, "CallTool: setting JSON output")
 32 | 	err = pdk.OutputJSON(output)
 33 | 	if err != nil {
 34 | 		pdk.SetError(err)
 35 | 		return -1
 36 | 	}
 37 | 
 38 | 	pdk.Log(pdk.LogDebug, "CallTool: returning")
 39 | 	return 0
 40 | }
 41 | 
 42 | //export complete
 43 | func _Complete() int32 {
 44 | 	var err error
 45 | 	_ = err
 46 | 	pdk.Log(pdk.LogDebug, "Complete: getting JSON input")
 47 | 	var input CompleteRequest
 48 | 	err = pdk.InputJSON(&input)
 49 | 	if err != nil {
 50 | 		pdk.SetError(err)
 51 | 		return -1
 52 | 	}
 53 | 
 54 | 	pdk.Log(pdk.LogDebug, "Complete: calling implementation function")
 55 | 	output, err := Complete(input)
 56 | 	if err != nil {
 57 | 		pdk.SetError(err)
 58 | 		return -1
 59 | 	}
 60 | 
 61 | 	if output == nil {
 62 | 		pdk.SetErrorString("Complete: output is nil")
 63 | 		return -1
 64 | 	}
 65 | 
 66 | 	pdk.Log(pdk.LogDebug, "Complete: setting JSON output")
 67 | 	err = pdk.OutputJSON(output)
 68 | 	if err != nil {
 69 | 		pdk.SetError(err)
 70 | 		return -1
 71 | 	}
 72 | 
 73 | 	pdk.Log(pdk.LogDebug, "Complete: returning")
 74 | 	return 0
 75 | }
 76 | 
 77 | //export get_prompt
 78 | func _GetPrompt() int32 {
 79 | 	var err error
 80 | 	_ = err
 81 | 	pdk.Log(pdk.LogDebug, "GetPrompt: getting JSON input")
 82 | 	var input GetPromptRequest
 83 | 	err = pdk.InputJSON(&input)
 84 | 	if err != nil {
 85 | 		pdk.SetError(err)
 86 | 		return -1
 87 | 	}
 88 | 
 89 | 	pdk.Log(pdk.LogDebug, "GetPrompt: calling implementation function")
 90 | 	output, err := GetPrompt(input)
 91 | 	if err != nil {
 92 | 		pdk.SetError(err)
 93 | 		return -1
 94 | 	}
 95 | 
 96 | 	if output == nil {
 97 | 		pdk.SetErrorString("GetPrompt: output is nil")
 98 | 		return -1
 99 | 	}
100 | 
101 | 	pdk.Log(pdk.LogDebug, "GetPrompt: setting JSON output")
102 | 	err = pdk.OutputJSON(output)
103 | 	if err != nil {
104 | 		pdk.SetError(err)
105 | 		return -1
106 | 	}
107 | 
108 | 	pdk.Log(pdk.LogDebug, "GetPrompt: returning")
109 | 	return 0
110 | }
111 | 
112 | //export list_prompts
113 | func _ListPrompts() int32 {
114 | 	var err error
115 | 	_ = err
116 | 	pdk.Log(pdk.LogDebug, "ListPrompts: getting JSON input")
117 | 	var input ListPromptsRequest
118 | 	err = pdk.InputJSON(&input)
119 | 	if err != nil {
120 | 		pdk.SetError(err)
121 | 		return -1
122 | 	}
123 | 
124 | 	pdk.Log(pdk.LogDebug, "ListPrompts: calling implementation function")
125 | 	output, err := ListPrompts(input)
126 | 	if err != nil {
127 | 		pdk.SetError(err)
128 | 		return -1
129 | 	}
130 | 
131 | 	if output == nil {
132 | 		pdk.SetErrorString("ListPrompts: output is nil")
133 | 		return -1
134 | 	}
135 | 
136 | 	pdk.Log(pdk.LogDebug, "ListPrompts: setting JSON output")
137 | 	err = pdk.OutputJSON(output)
138 | 	if err != nil {
139 | 		pdk.SetError(err)
140 | 		return -1
141 | 	}
142 | 
143 | 	pdk.Log(pdk.LogDebug, "ListPrompts: returning")
144 | 	return 0
145 | }
146 | 
147 | //export list_resource_templates
148 | func _ListResourceTemplates() int32 {
149 | 	var err error
150 | 	_ = err
151 | 	pdk.Log(pdk.LogDebug, "ListResourceTemplates: getting JSON input")
152 | 	var input ListResourceTemplatesRequest
153 | 	err = pdk.InputJSON(&input)
154 | 	if err != nil {
155 | 		pdk.SetError(err)
156 | 		return -1
157 | 	}
158 | 
159 | 	pdk.Log(pdk.LogDebug, "ListResourceTemplates: calling implementation function")
160 | 	output, err := ListResourceTemplates(input)
161 | 	if err != nil {
162 | 		pdk.SetError(err)
163 | 		return -1
164 | 	}
165 | 
166 | 	if output == nil {
167 | 		pdk.SetErrorString("ListResourceTemplates: output is nil")
168 | 		return -1
169 | 	}
170 | 
171 | 	pdk.Log(pdk.LogDebug, "ListResourceTemplates: setting JSON output")
172 | 	err = pdk.OutputJSON(output)
173 | 	if err != nil {
174 | 		pdk.SetError(err)
175 | 		return -1
176 | 	}
177 | 
178 | 	pdk.Log(pdk.LogDebug, "ListResourceTemplates: returning")
179 | 	return 0
180 | }
181 | 
182 | //export list_resources
183 | func _ListResources() int32 {
184 | 	var err error
185 | 	_ = err
186 | 	pdk.Log(pdk.LogDebug, "ListResources: getting JSON input")
187 | 	var input ListResourcesRequest
188 | 	err = pdk.InputJSON(&input)
189 | 	if err != nil {
190 | 		pdk.SetError(err)
191 | 		return -1
192 | 	}
193 | 
194 | 	pdk.Log(pdk.LogDebug, "ListResources: calling implementation function")
195 | 	output, err := ListResources(input)
196 | 	if err != nil {
197 | 		pdk.SetError(err)
198 | 		return -1
199 | 	}
200 | 
201 | 	if output == nil {
202 | 		pdk.SetErrorString("ListResources: output is nil")
203 | 		return -1
204 | 	}
205 | 
206 | 	pdk.Log(pdk.LogDebug, "ListResources: setting JSON output")
207 | 	err = pdk.OutputJSON(output)
208 | 	if err != nil {
209 | 		pdk.SetError(err)
210 | 		return -1
211 | 	}
212 | 
213 | 	pdk.Log(pdk.LogDebug, "ListResources: returning")
214 | 	return 0
215 | }
216 | 
217 | //export list_tools
218 | func _ListTools() int32 {
219 | 	var err error
220 | 	_ = err
221 | 	pdk.Log(pdk.LogDebug, "ListTools: getting JSON input")
222 | 	var input ListToolsRequest
223 | 	err = pdk.InputJSON(&input)
224 | 	if err != nil {
225 | 		pdk.SetError(err)
226 | 		return -1
227 | 	}
228 | 
229 | 	pdk.Log(pdk.LogDebug, "ListTools: calling implementation function")
230 | 	output, err := ListTools(input)
231 | 	if err != nil {
232 | 		pdk.SetError(err)
233 | 		return -1
234 | 	}
235 | 
236 | 	if output == nil {
237 | 		pdk.SetErrorString("ListTools: output is nil")
238 | 		return -1
239 | 	}
240 | 
241 | 	pdk.Log(pdk.LogDebug, "ListTools: setting JSON output")
242 | 	err = pdk.OutputJSON(output)
243 | 	if err != nil {
244 | 		pdk.SetError(err)
245 | 		return -1
246 | 	}
247 | 
248 | 	pdk.Log(pdk.LogDebug, "ListTools: returning")
249 | 	return 0
250 | }
251 | 
252 | //export on_roots_list_changed
253 | func _OnRootsListChanged() int32 {
254 | 	var err error
255 | 	_ = err
256 | 	pdk.Log(pdk.LogDebug, "OnRootsListChanged: getting JSON input")
257 | 	var input PluginNotificationContext
258 | 	err = pdk.InputJSON(&input)
259 | 	if err != nil {
260 | 		pdk.SetError(err)
261 | 		return -1
262 | 	}
263 | 
264 | 	pdk.Log(pdk.LogDebug, "OnRootsListChanged: calling implementation function")
265 | 	err = OnRootsListChanged(input)
266 | 	if err != nil {
267 | 		pdk.SetError(err)
268 | 		return -1
269 | 	}
270 | 
271 | 	pdk.Log(pdk.LogDebug, "OnRootsListChanged: returning")
272 | 	return 0
273 | }
274 | 
275 | //export read_resource
276 | func _ReadResource() int32 {
277 | 	var err error
278 | 	_ = err
279 | 	pdk.Log(pdk.LogDebug, "ReadResource: getting JSON input")
280 | 	var input ReadResourceRequest
281 | 	err = pdk.InputJSON(&input)
282 | 	if err != nil {
283 | 		pdk.SetError(err)
284 | 		return -1
285 | 	}
286 | 
287 | 	pdk.Log(pdk.LogDebug, "ReadResource: calling implementation function")
288 | 	output, err := ReadResource(input)
289 | 	if err != nil {
290 | 		pdk.SetError(err)
291 | 		return -1
292 | 	}
293 | 
294 | 	if output == nil {
295 | 		pdk.SetErrorString("ReadResource: output is nil")
296 | 		return -1
297 | 	}
298 | 
299 | 	pdk.Log(pdk.LogDebug, "ReadResource: setting JSON output")
300 | 	err = pdk.OutputJSON(output)
301 | 	if err != nil {
302 | 		pdk.SetError(err)
303 | 		return -1
304 | 	}
305 | 
306 | 	pdk.Log(pdk.LogDebug, "ReadResource: returning")
307 | 	return 0
308 | }
309 | 
```

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

```go
  1 | // Note: run `go doc -all` in this package to see all of the types and functions available.
  2 | // ./pdk.gen.go contains the domain types from the host where your plugin will run.
  3 | package main
  4 | 
  5 | import (
  6 | 	"fmt"
  7 | 
  8 | 	"github.com/extism/go-pdk"
  9 | )
 10 | 
 11 | // Called when the tool is invoked.
 12 | // If you support multiple tools, you must switch on the input.params.name to detect which tool is being called.
 13 | // The name will match one of the tool names returned from "describe".
 14 | // It takes CallToolRequest as input (The incoming tool request from the LLM)
 15 | // And returns CallToolResult (The servlet's response to the given tool call)
 16 | func Call(input CallToolRequest) (CallToolResult, error) {
 17 | 	apiKey, ok := pdk.GetConfig("api-key")
 18 | 	if !ok {
 19 | 		return CallToolResult{
 20 | 			IsError: some(true),
 21 | 			Content: []Content{{
 22 | 				Type: ContentTypeText,
 23 | 				Text: some("No api-key configured"),
 24 | 			}},
 25 | 		}, nil
 26 | 	}
 27 | 	args := input.Params.Arguments.(map[string]interface{})
 28 | 	pdk.Log(pdk.LogDebug, fmt.Sprint("Args: ", args))
 29 | 	switch input.Params.Name {
 30 | 	case ListIssuesTool.Name:
 31 | 		owner, _ := args["owner"].(string)
 32 | 		repo, _ := args["repo"].(string)
 33 | 		return issueList(apiKey, owner, repo, args)
 34 | 	case GetIssueTool.Name:
 35 | 		owner, _ := args["owner"].(string)
 36 | 		repo, _ := args["repo"].(string)
 37 | 		issue, _ := args["issue"].(float64)
 38 | 		return issueGet(apiKey, owner, repo, int(issue))
 39 | 	case AddIssueCommentTool.Name:
 40 | 		owner, _ := args["owner"].(string)
 41 | 		repo, _ := args["repo"].(string)
 42 | 		issue, _ := args["issue"].(float64)
 43 | 		body, _ := args["body"].(string)
 44 | 		return issueAddComment(apiKey, owner, repo, int(issue), body)
 45 | 	case CreateIssueTool.Name:
 46 | 		owner, _ := args["owner"].(string)
 47 | 		repo, _ := args["repo"].(string)
 48 | 		data := issueFromArgs(args)
 49 | 		return issueCreate(apiKey, owner, repo, data)
 50 | 	case UpdateIssueTool.Name:
 51 | 		owner, _ := args["owner"].(string)
 52 | 		repo, _ := args["repo"].(string)
 53 | 		issue, _ := args["issue"].(float64)
 54 | 		data := issueFromArgs(args)
 55 | 		return issueUpdate(apiKey, owner, repo, int(issue), data)
 56 | 
 57 | 	case GetFileContentsTool.Name:
 58 | 		owner, _ := args["owner"].(string)
 59 | 		repo, _ := args["repo"].(string)
 60 | 		path, _ := args["path"].(string)
 61 | 		branch, _ := args["branch"].(string)
 62 | 		res := filesGetContents(apiKey, owner, repo, path, &branch)
 63 | 		return res, nil
 64 | 	case CreateOrUpdateFileTool.Name:
 65 | 		owner, _ := args["owner"].(string)
 66 | 		repo, _ := args["repo"].(string)
 67 | 		path, _ := args["path"].(string)
 68 | 		file := fileCreateFromArgs(args)
 69 | 		return filesCreateOrUpdate(apiKey, owner, repo, path, file)
 70 | 
 71 | 	case CreateBranchTool.Name:
 72 | 		owner, _ := args["owner"].(string)
 73 | 		repo, _ := args["repo"].(string)
 74 | 		from, _ := args["branch"].(string)
 75 | 		var maybeBranch *string
 76 | 		if branch, ok := args["from_branch"].(string); ok {
 77 | 			maybeBranch = &branch
 78 | 		}
 79 | 		return branchCreate(apiKey, owner, repo, from, maybeBranch), nil
 80 | 
 81 | 	case ListPullRequestsTool.Name:
 82 | 		owner, _ := args["owner"].(string)
 83 | 		repo, _ := args["repo"].(string)
 84 | 		return pullRequestList(apiKey, owner, repo, args)
 85 | 
 86 | 	case CreatePullRequestTool.Name:
 87 | 		owner, _ := args["owner"].(string)
 88 | 		repo, _ := args["repo"].(string)
 89 | 		pr := branchPullRequestSchemaFromArgs(args)
 90 | 		return branchCreatePullRequest(apiKey, owner, repo, pr), nil
 91 | 
 92 | 	case PushFilesTool.Name:
 93 | 		owner, _ := args["owner"].(string)
 94 | 		repo, _ := args["repo"].(string)
 95 | 		branch, _ := args["branch"].(string)
 96 | 		message, _ := args["message"].(string)
 97 | 		files := filePushFromArgs(args)
 98 | 		return filesPush(apiKey, owner, repo, branch, message, files), nil
 99 | 
100 | 	case ListReposTool.Name:
101 | 		owner, _ := args["owner"].(string)
102 | 		return reposList(apiKey, owner, args)
103 | 
104 | 	case GetRepositoryCollaboratorsTool.Name:
105 | 		owner, _ := args["owner"].(string)
106 | 		repo, _ := args["repo"].(string)
107 | 		return reposGetCollaborators(apiKey, owner, repo, args)
108 | 
109 | 	case GetRepositoryContributorsTool.Name:
110 | 		owner, _ := args["owner"].(string)
111 | 		repo, _ := args["repo"].(string)
112 | 		return reposGetContributors(apiKey, owner, repo, args)
113 | 
114 | 	case GetRepositoryDetailsTool.Name:
115 | 		owner, _ := args["owner"].(string)
116 | 		repo, _ := args["repo"].(string)
117 | 		return reposGetDetails(apiKey, owner, repo)
118 | 
119 | 	case CreateGistTool.Name:
120 | 		description, _ := args["description"].(string)
121 | 		files, _ := args["files"].(map[string]any)
122 | 		return gistCreate(apiKey, description, files), nil
123 | 
124 | 	case GetGistTool.Name:
125 | 		gistId, _ := args["gist_id"].(string)
126 | 		return gistGet(apiKey, gistId), nil
127 | 
128 | 	case UpdateGistTool.Name:
129 | 		gistId, _ := args["gist_id"].(string)
130 | 		description, _ := args["description"].(string)
131 | 		files, _ := args["files"].(map[string]any)
132 | 		return gistUpdate(apiKey, gistId, description, files), nil
133 | 
134 | 	case DeleteGistTool.Name:
135 | 		gistId, _ := args["gist_id"].(string)
136 | 		return gistDelete(apiKey, gistId), nil
137 | 
138 | 	default:
139 | 		return CallToolResult{
140 | 			IsError: some(true),
141 | 			Content: []Content{{
142 | 				Type: ContentTypeText,
143 | 				Text: some("Unknown tool " + input.Params.Name),
144 | 			}},
145 | 		}, nil
146 | 	}
147 | 
148 | }
149 | 
150 | func Describe() (ListToolsResult, error) {
151 | 	toolsets := [][]ToolDescription{
152 | 		IssueTools,
153 | 		FileTools,
154 | 		BranchTools,
155 | 		RepoTools,
156 | 		GistTools,
157 | 	}
158 | 
159 | 	tools := []ToolDescription{}
160 | 
161 | 	for _, toolset := range toolsets {
162 | 		tools = append(tools, toolset...)
163 | 	}
164 | 
165 | 	// Ensure each tool's InputSchema has a required field
166 | 	for i := range tools {
167 | 		// Check if InputSchema is a map[string]interface{}
168 | 		if schema, ok := tools[i].InputSchema.(map[string]interface{}); ok {
169 | 			// Check if required field is missing
170 | 			if _, exists := schema["required"]; !exists {
171 | 				// Add an empty required array if it doesn't exist
172 | 				schema["required"] = []string{}
173 | 				tools[i].InputSchema = schema
174 | 			}
175 | 		}
176 | 	}
177 | 
178 | 	return ListToolsResult{
179 | 		Tools: tools,
180 | 	}, nil
181 | }
182 | 
183 | func some[T any](t T) *T {
184 | 	return &t
185 | }
186 | 
187 | type SchemaProperty struct {
188 | 	Type                 string  `json:"type"`
189 | 	Description          string  `json:"description,omitempty"`
190 | 	AdditionalProperties *schema `json:"additionalProperties,omitempty"`
191 | 	Items                *schema `json:"items,omitempty"`
192 | }
193 | 
194 | func prop(tpe, description string) SchemaProperty {
195 | 	return SchemaProperty{Type: tpe, Description: description}
196 | }
197 | 
198 | func arrprop(tpe, description, itemstpe string) SchemaProperty {
199 | 	items := schema{"type": itemstpe}
200 | 	return SchemaProperty{Type: tpe, Description: description, Items: &items}
201 | }
202 | 
203 | type schema = map[string]interface{}
204 | type props = map[string]SchemaProperty
205 | 
```

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

```rust
  1 | mod pdk;
  2 | 
  3 | use std::collections::BTreeMap;
  4 | 
  5 | use extism_pdk::*;
  6 | use json::Value;
  7 | use pdk::types::{
  8 |     CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
  9 | };
 10 | use serde_json::json;
 11 | 
 12 | pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
 13 |     match input.params.name.as_str() {
 14 |         "mvn_fetch_deps" => mvn_fetch_deps(input),
 15 |         _ => Ok(CallToolResult {
 16 |             is_error: Some(true),
 17 |             content: vec![Content {
 18 |                 annotations: None,
 19 |                 text: Some(format!("Unknown tool: {}", input.params.name)),
 20 |                 mime_type: None,
 21 |                 r#type: ContentType::Text,
 22 |                 data: None,
 23 |             }],
 24 |         }),
 25 |     }
 26 | }
 27 | 
 28 | fn mvn_fetch_deps(input: CallToolRequest) -> Result<CallToolResult, Error> {
 29 |     use quick_xml::Reader;
 30 |     use quick_xml::events::Event;
 31 |     use serde_json::json;
 32 | 
 33 |     let args = input.params.arguments.unwrap_or_default();
 34 |     let group = match args.get("group") {
 35 |         Some(Value::String(s)) => s,
 36 |         _ => {
 37 |             return Ok(CallToolResult {
 38 |                 is_error: Some(true),
 39 |                 content: vec![Content {
 40 |                     annotations: None,
 41 |                     text: Some("Missing 'group' argument".into()),
 42 |                     mime_type: None,
 43 |                     r#type: ContentType::Text,
 44 |                     data: None,
 45 |                 }],
 46 |             });
 47 |         }
 48 |     };
 49 |     let artifact = match args.get("artifact") {
 50 |         Some(Value::String(s)) => s,
 51 |         _ => {
 52 |             return Ok(CallToolResult {
 53 |                 is_error: Some(true),
 54 |                 content: vec![Content {
 55 |                     annotations: None,
 56 |                     text: Some("Missing 'artifact' argument".into()),
 57 |                     mime_type: None,
 58 |                     r#type: ContentType::Text,
 59 |                     data: None,
 60 |                 }],
 61 |             });
 62 |         }
 63 |     };
 64 |     let version = match args.get("version") {
 65 |         Some(Value::String(s)) => s,
 66 |         _ => {
 67 |             return Ok(CallToolResult {
 68 |                 is_error: Some(true),
 69 |                 content: vec![Content {
 70 |                     annotations: None,
 71 |                     text: Some("Missing 'version' argument".into()),
 72 |                     mime_type: None,
 73 |                     r#type: ContentType::Text,
 74 |                     data: None,
 75 |                 }],
 76 |             });
 77 |         }
 78 |     };
 79 | 
 80 |     let group_path = group.replace('.', "/");
 81 |     let pom_url = format!(
 82 |         "https://repo1.maven.org/maven2/{}/{}/{}/{}-{}.pom",
 83 |         group_path, artifact, version, artifact, version
 84 |     );
 85 | 
 86 |     let mut req = HttpRequest {
 87 |         url: pom_url.clone(),
 88 |         headers: BTreeMap::new(),
 89 |         method: Some("GET".to_string()),
 90 |     };
 91 |     req.headers
 92 |         .insert("User-Agent".to_string(), "maven-plugin/1.0".to_string());
 93 | 
 94 |     let res = match http::request::<()>(&req, None) {
 95 |         Ok(r) => r,
 96 |         Err(e) => {
 97 |             return Ok(CallToolResult {
 98 |                 is_error: Some(true),
 99 |                 content: vec![Content {
100 |                     annotations: None,
101 |                     text: Some(format!("Failed to fetch POM: {}", e)),
102 |                     mime_type: None,
103 |                     r#type: ContentType::Text,
104 |                     data: None,
105 |                 }],
106 |             });
107 |         }
108 |     };
109 |     let body = res.body();
110 |     let xml = String::from_utf8_lossy(body.as_slice());
111 | 
112 |     // Parse dependencies
113 |     let mut reader = Reader::from_str(&xml);
114 |     reader.trim_text(true);
115 |     let mut buf = Vec::new();
116 |     let mut dependencies = Vec::new();
117 |     let mut in_dependencies = false;
118 |     let mut current = serde_json::Map::new();
119 |     let mut current_tag = String::new();
120 | 
121 |     loop {
122 |         match reader.read_event_into(&mut buf) {
123 |             Ok(Event::Start(ref e)) => {
124 |                 let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
125 |                 if tag == "dependencies" {
126 |                     in_dependencies = true;
127 |                 } else if in_dependencies && tag == "dependency" {
128 |                     current = serde_json::Map::new();
129 |                 } else if in_dependencies {
130 |                     current_tag = tag;
131 |                 }
132 |             }
133 |             Ok(Event::End(ref e)) => {
134 |                 let tag = String::from_utf8_lossy(e.name().as_ref()).to_string();
135 |                 if tag == "dependencies" {
136 |                     in_dependencies = false;
137 |                 } else if in_dependencies && tag == "dependency" {
138 |                     dependencies.push(json!(current));
139 |                 } else if in_dependencies {
140 |                     current_tag.clear();
141 |                 }
142 |             }
143 |             Ok(Event::Text(e)) => {
144 |                 if in_dependencies && !current_tag.is_empty() {
145 |                     current.insert(current_tag.clone(), json!(e.unescape().unwrap_or_default()));
146 |                 }
147 |             }
148 |             Ok(Event::Eof) => break,
149 |             Err(_) => break,
150 |             _ => {}
151 |         }
152 |         buf.clear();
153 |     }
154 | 
155 |     Ok(CallToolResult {
156 |         is_error: None,
157 |         content: vec![Content {
158 |             annotations: None,
159 |             text: Some(json!({"dependencies": dependencies}).to_string()),
160 |             mime_type: Some("application/json".to_string()),
161 |             r#type: ContentType::Text,
162 |             data: None,
163 |         }],
164 |     })
165 | }
166 | 
167 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
168 |     Ok(ListToolsResult{
169 |         tools: vec![
170 |             ToolDescription {
171 |                 name: "mvn_fetch_deps".into(),
172 |                 description:  "Fetches the dependencies of a Maven package by group, artifact, and version from Maven Central.".into(),
173 |                 input_schema: json!({
174 |                     "type": "object",
175 |                     "properties": {
176 |                         "group": {
177 |                             "type": "string",
178 |                             "description": "The Maven groupId",
179 |                         },
180 |                         "artifact": {
181 |                             "type": "string",
182 |                             "description": "The Maven artifactId",
183 |                         },
184 |                         "version": {
185 |                             "type": "string",
186 |                             "description": "The Maven version",
187 |                         },
188 |                     },
189 |                     "required": ["group", "artifact", "version"],
190 |                 })
191 |                 .as_object()
192 |                 .unwrap()
193 |                 .clone(),
194 |             },
195 |         ],
196 |     })
197 | }
198 | 
```

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

```rust
  1 | mod pdk;
  2 | 
  3 | use std::collections::BTreeMap;
  4 | 
  5 | use extism_pdk::*;
  6 | use json::Value;
  7 | use pdk::types::{
  8 |     CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
  9 | };
 10 | use serde_json::json;
 11 | 
 12 | pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
 13 |     match input.params.name.as_str() {
 14 |         "gomodule_latest_version" => latest_version(input),
 15 |         "gomodule_info" => module_info(input),
 16 |         _ => Ok(CallToolResult {
 17 |             is_error: Some(true),
 18 |             content: vec![Content {
 19 |                 annotations: None,
 20 |                 text: Some(format!("Unknown tool: {}", input.params.name)),
 21 |                 mime_type: None,
 22 |                 r#type: ContentType::Text,
 23 |                 data: None,
 24 |             }],
 25 |         }),
 26 |     }
 27 | }
 28 | 
 29 | fn module_info(input: CallToolRequest) -> Result<CallToolResult, Error> {
 30 |     let args = input.params.arguments.unwrap_or_default();
 31 |     if let Some(Value::String(module_names)) = args.get("module_names") {
 32 |         let module_names: Vec<&str> = module_names.split(',').map(|s| s.trim()).collect();
 33 |         let mut results = Vec::new();
 34 | 
 35 |         for module_name in module_names {
 36 |             let mut req = HttpRequest {
 37 |                 url: format!("https://proxy.golang.org/{}/@latest", module_name),
 38 |                 headers: BTreeMap::new(),
 39 |                 method: Some("GET".to_string()),
 40 |             };
 41 | 
 42 |             req.headers
 43 |                 .insert("User-Agent".to_string(), "hyper-mcp/1.0".to_string());
 44 | 
 45 |             let res = http::request::<()>(&req, None)?;
 46 | 
 47 |             let body = res.body();
 48 |             let json_str = String::from_utf8_lossy(body.as_slice());
 49 | 
 50 |             let json: serde_json::Value = serde_json::from_str(&json_str)?;
 51 | 
 52 |             // TODO: figure out how to get module license
 53 |             results.push(json);
 54 |         }
 55 | 
 56 |         if !results.is_empty() {
 57 |             Ok(CallToolResult {
 58 |                 is_error: None,
 59 |                 content: vec![Content {
 60 |                     annotations: None,
 61 |                     text: Some(serde_json::to_string(&results)?),
 62 |                     mime_type: Some("text/plain".to_string()),
 63 |                     r#type: ContentType::Text,
 64 |                     data: None,
 65 |                 }],
 66 |             })
 67 |         } else {
 68 |             Ok(CallToolResult {
 69 |                 is_error: Some(true),
 70 |                 content: vec![Content {
 71 |                     annotations: None,
 72 |                     text: Some("Failed to get module information".into()),
 73 |                     mime_type: None,
 74 |                     r#type: ContentType::Text,
 75 |                     data: None,
 76 |                 }],
 77 |             })
 78 |         }
 79 |     } else {
 80 |         Ok(CallToolResult {
 81 |             is_error: Some(true),
 82 |             content: vec![Content {
 83 |                 annotations: None,
 84 |                 text: Some("Please provide module names".into()),
 85 |                 mime_type: None,
 86 |                 r#type: ContentType::Text,
 87 |                 data: None,
 88 |             }],
 89 |         })
 90 |     }
 91 | }
 92 | 
 93 | fn latest_version(input: CallToolRequest) -> Result<CallToolResult, Error> {
 94 |     let args = input.params.arguments.unwrap_or_default();
 95 |     if let Some(Value::String(module_names)) = args.get("module_names") {
 96 |         let module_names: Vec<&str> = module_names.split(',').map(|s| s.trim()).collect();
 97 |         let mut results = BTreeMap::new();
 98 | 
 99 |         for module_name in module_names {
100 |             let mut req = HttpRequest {
101 |                 url: format!("https://proxy.golang.org/{}/@latest", module_name),
102 |                 headers: BTreeMap::new(),
103 |                 method: Some("GET".to_string()),
104 |             };
105 | 
106 |             req.headers
107 |                 .insert("User-Agent".to_string(), "hyper-mcp/1.0".to_string());
108 | 
109 |             let res = http::request::<()>(&req, None)?;
110 | 
111 |             let body = res.body();
112 |             let json_str = String::from_utf8_lossy(body.as_slice());
113 | 
114 |             let json: serde_json::Value = serde_json::from_str(&json_str)?;
115 | 
116 |             if let Some(version) = json["Version"].as_str() {
117 |                 results.insert(module_name.to_string(), version.to_string());
118 |             }
119 |         }
120 | 
121 |         if !results.is_empty() {
122 |             Ok(CallToolResult {
123 |                 is_error: None,
124 |                 content: vec![Content {
125 |                     annotations: None,
126 |                     text: Some(serde_json::to_string(&results)?),
127 |                     mime_type: Some("text/plain".to_string()),
128 |                     r#type: ContentType::Text,
129 |                     data: None,
130 |                 }],
131 |             })
132 |         } else {
133 |             Ok(CallToolResult {
134 |                 is_error: Some(true),
135 |                 content: vec![Content {
136 |                     annotations: None,
137 |                     text: Some("Failed to get latest versions".into()),
138 |                     mime_type: None,
139 |                     r#type: ContentType::Text,
140 |                     data: None,
141 |                 }],
142 |             })
143 |         }
144 |     } else {
145 |         Ok(CallToolResult {
146 |             is_error: Some(true),
147 |             content: vec![Content {
148 |                 annotations: None,
149 |                 text: Some("Please provide module names".into()),
150 |                 mime_type: None,
151 |                 r#type: ContentType::Text,
152 |                 data: None,
153 |             }],
154 |         })
155 |     }
156 | }
157 | 
158 | pub(crate) fn describe() -> Result<ListToolsResult, Error> {
159 |     Ok(ListToolsResult {
160 |         tools: vec![
161 |             ToolDescription {
162 |                 name: "gomodule_latest_version".into(),
163 |                 description: "Fetches the latest version of multiple Go modules. Assume it's github.com if not specified".into(),
164 |                 input_schema: json!({
165 |                     "type": "object",
166 |                     "properties": {
167 |                         "module_names": {
168 |                             "type": "string",
169 |                             "description": "Comma-separated list of Go module names to get the latest versions for",
170 |                         },
171 |                     },
172 |                     "required": ["module_names"],
173 |                 })
174 |                 .as_object()
175 |                 .unwrap()
176 |                 .clone(),
177 |             },
178 |             ToolDescription {
179 |                 name: "gomodule_info".into(),
180 |                 description: "Fetches detailed information about multiple Go modules. Assume it's github.com if not specified".into(),
181 |                 input_schema: json!({
182 |                     "type": "object",
183 |                     "properties": {
184 |                         "module_names": {
185 |                             "type": "string",
186 |                             "description": "Comma-separated list of Go module names to get information for",
187 |                         },
188 |                     },
189 |                     "required": ["module_names"],
190 |                 })
191 |                 .as_object()
192 |                 .unwrap()
193 |                 .clone(),
194 |             },
195 |         ],
196 |     })
197 | }
198 | 
```
Page 2/11FirstPrevNextLast