This is page 4 of 8. Use http://codebase.md/tuananh/hyper-mcp?page={x} to view the full context.
# Directory Structure
```
├── .cursor
│ └── rules
│ └── print-ctx-size.mdc
├── .dockerignore
├── .github
│ ├── renovate.json5
│ └── workflows
│ ├── ci.yml
│ ├── nightly.yml
│ └── release.yml
├── .gitignore
├── .gitmodules
├── .hadolint.yaml
├── .pre-commit-config.yaml
├── .windsurf
│ └── rules
│ ├── print-ctx-size.md
│ └── think.md
├── assets
│ ├── cursor-mcp-1.png
│ ├── cursor-mcp.png
│ ├── eval-py.jpg
│ └── logo.png
├── Cargo.lock
├── Cargo.toml
├── config.example.json
├── config.example.yaml
├── CREATING_PLUGINS.md
├── DEPLOYMENT.md
├── Dockerfile
├── examples
│ └── plugins
│ ├── v1
│ │ ├── arxiv
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── context7
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── crates-io
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── crypto-price
│ │ │ ├── Dockerfile
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── main.go
│ │ │ ├── pdk.gen.go
│ │ │ └── README.md
│ │ ├── eval-py
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── fetch
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── fs
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── github
│ │ │ ├── .gitignore
│ │ │ ├── branches.go
│ │ │ ├── Dockerfile
│ │ │ ├── files.go
│ │ │ ├── gists.go
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── issues.go
│ │ │ ├── main.go
│ │ │ ├── pdk.gen.go
│ │ │ ├── README.md
│ │ │ └── repo.go
│ │ ├── gitlab
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── gomodule
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── hash
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.lock
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── maven
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── meme-generator
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── generate_embedded.py
│ │ │ ├── README.md
│ │ │ ├── src
│ │ │ │ ├── embedded.rs
│ │ │ │ ├── lib.rs
│ │ │ │ └── pdk.rs
│ │ │ └── templates.json
│ │ ├── memory
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── myip
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.lock
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── qdrant
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ ├── pdk.rs
│ │ │ └── qdrant_client.rs
│ │ ├── qr-code
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.lock
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── serper
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── sqlite
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── think
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ ├── time
│ │ │ ├── .cargo
│ │ │ │ └── config.toml
│ │ │ ├── .gitignore
│ │ │ ├── Cargo.toml
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ ├── src
│ │ │ │ ├── lib.rs
│ │ │ │ └── pdk.rs
│ │ │ └── time.wasm
│ │ └── tool-list-changed
│ │ ├── .gitignore
│ │ ├── Cargo.toml
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── lib.rs
│ │ │ └── pdk.rs
│ │ └── tool_list_changed.wasm
│ └── v2
│ └── rstime
│ ├── .cargo
│ │ └── config.toml
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── Dockerfile
│ ├── README.md
│ ├── rstime.wasm
│ └── src
│ ├── lib.rs
│ └── pdk
│ ├── exports.rs
│ ├── imports.rs
│ ├── mod.rs
│ └── types.rs
├── iac
│ ├── .terraform.lock.hcl
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
├── justfile
├── LICENSE
├── README.md
├── RUNTIME_CONFIG.md
├── rust-toolchain.toml
├── server.json
├── SKIP_TOOLS_GUIDE.md
├── src
│ ├── cli.rs
│ ├── config.rs
│ ├── https_auth.rs
│ ├── logging.rs
│ ├── main.rs
│ ├── naming.rs
│ ├── plugin.rs
│ ├── service.rs
│ └── wasm
│ ├── http.rs
│ ├── mod.rs
│ ├── oci.rs
│ └── s3.rs
├── templates
│ └── plugins
│ ├── go
│ │ ├── .gitignore
│ │ ├── Dockerfile
│ │ ├── exports.go
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── imports.go
│ │ ├── main.go
│ │ ├── README.md
│ │ └── types.go
│ ├── README.md
│ └── rust
│ ├── .cargo
│ │ └── config.toml
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── Dockerfile
│ ├── README.md
│ └── src
│ ├── lib.rs
│ └── pdk
│ ├── exports.rs
│ ├── imports.rs
│ ├── mod.rs
│ └── types.rs
├── tests
│ └── fixtures
│ ├── config_with_auths.json
│ ├── config_with_auths.yaml
│ ├── documentation_example.json
│ ├── documentation_example.yaml
│ ├── invalid_auth_config.yaml
│ ├── invalid_plugin_name.yaml
│ ├── invalid_structure.yaml
│ ├── invalid_url.yaml
│ ├── keyring_auth_config.yaml
│ ├── skip_tools_examples.yaml
│ ├── unsupported_config.txt
│ ├── valid_config.json
│ └── valid_config.yaml
└── xtp-plugin-schema.json
```
# Files
--------------------------------------------------------------------------------
/examples/plugins/v1/github/files.go:
--------------------------------------------------------------------------------
```go
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"github.com/extism/go-pdk"
)
var (
GetFileContentsTool = ToolDescription{
Name: "gh-get-file-contents",
Description: "Get the contents of a file or a directory in a GitHub repository",
InputSchema: schema{
"type": "object",
"properties": props{
"owner": prop("string", "The owner of the repository"),
"repo": prop("string", "The repository name"),
"path": prop("string", "The path of the file"),
"branch": prop("string", "(optional string): Branch to get contents from"),
},
"required": []string{"owner", "repo", "path"},
},
}
CreateOrUpdateFileTool = ToolDescription{
Name: "gh-create-or-update-file",
Description: "Create or update a file in a GitHub repository",
InputSchema: schema{
"type": "object",
"properties": props{
"owner": prop("string", "The owner of the repository"),
"repo": prop("string", "The repository name"),
"path": prop("string", "The path of the file"),
"content": prop("string", "The content of the file"),
"message": prop("string", "The commit message"),
"branch": prop("string", "The branch name"),
"sha": prop("string", "(optional) The sha of the file, required for updates"),
},
"required": []string{"owner", "repo", "path", "content", "message", "branch"},
},
}
PushFilesTool = ToolDescription{
Name: "gh-push-files",
Description: "Push files to a GitHub repository",
InputSchema: schema{
"type": "object",
"properties": props{
"owner": prop("string", "The owner of the repository"),
"repo": prop("string", "The repository name"),
"branch": prop("string", "The branch name to push to"),
"message": prop("string", "The commit message"),
"files": SchemaProperty{
Type: "array",
Description: "Array of files to push",
Items: &schema{
"type": "object",
"properties": props{
"path": prop("string", "The path of the file"),
"content": prop("string", "The content of the file"),
},
},
},
},
},
}
FileTools = []ToolDescription{
GetFileContentsTool,
CreateOrUpdateFileTool,
PushFilesTool,
}
)
type FileCreate struct {
Content string `json:"content"`
Message string `json:"message"`
Branch string `json:"branch"`
Sha *string `json:"sha,omitempty"`
}
func fileCreateFromArgs(args map[string]interface{}) FileCreate {
file := FileCreate{}
if content, ok := args["content"].(string); ok {
b64c := base64.StdEncoding.EncodeToString([]byte(content))
file.Content = b64c
}
if message, ok := args["message"].(string); ok {
file.Message = message
}
if branch, ok := args["branch"].(string); ok {
file.Branch = branch
}
if sha, ok := args["sha"].(string); ok {
file.Sha = some(sha)
}
return file
}
func filesCreateOrUpdate(apiKey string, owner string, repo string, path string, file FileCreate) (CallToolResult, error) {
if file.Sha == nil {
uc, err := filesGetContentsInternal(apiKey, owner, repo, path, &file.Branch)
if err != nil {
pdk.Log(pdk.LogDebug, "File does not exist, creating it")
} else if !uc.isArray {
sha := uc.FileContent.Sha
file.Sha = &sha
}
}
url := fmt.Sprint("https://api.github.com/repos/", owner, "/", repo, "/contents/", path)
req := pdk.NewHTTPRequest(pdk.MethodPut, url)
req.SetHeader("Authorization", fmt.Sprint("token ", apiKey))
req.SetHeader("Accept", "application/vnd.github.v3+json")
req.SetHeader("User-Agent", "github-mcpx-servlet")
res, err := json.Marshal(file)
if err != nil {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(fmt.Sprint("Failed to marshal file: ", err)),
}},
}, nil
}
req.SetBody([]byte(res))
resp := req.Send()
if resp.Status() != 201 {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(fmt.Sprint("Failed to create or update file: ", resp.Status(), " ", string(resp.Body()))),
}},
}, nil
}
return CallToolResult{
Content: []Content{{
Type: ContentTypeText,
Text: some(string(resp.Body())),
}},
}, nil
}
type UnionContent struct {
isArray bool
FileContent FileContent
DirectoryContents []DirectoryContent
}
type FileContent struct {
Type string `json:"type"`
Encoding string `json:"encoding"`
Size int `json:"size"`
Name string `json:"name"`
Path string `json:"path"`
Content string `json:"content"`
Sha string `json:"sha"`
Url string `json:"url"`
GitUrl string `json:"git_url"`
HtmlUrl string `json:"html_url"`
DownloadUrl string `json:"download_url"`
}
type DirectoryContent struct {
Type string `json:"type"`
Size int `json:"size"`
Name string `json:"name"`
Path string `json:"path"`
Sha string `json:"sha"`
Url string `json:"url"`
GitUrl string `json:"git_url"`
HtmlUrl string `json:"html_url"`
DownloadUrl *string `json:"download_url"`
}
func filesGetContents(apiKey string, owner string, repo string, path string, branch *string) CallToolResult {
res, err := filesGetContentsInternal(apiKey, owner, repo, path, branch)
if err == nil {
var v []byte
if res.isArray {
v, err = json.Marshal(res.DirectoryContents)
} else {
v, err = json.Marshal(res.FileContent)
}
if err == nil {
return CallToolResult{
Content: []Content{{
Type: ContentTypeText,
Text: some(string(v)),
}},
}
}
}
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(err.Error()),
}},
}
}
func filesGetContentsInternal(apiKey string, owner string, repo string, path string, branch *string) (UnionContent, error) {
u := fmt.Sprint("https://api.github.com/repos/", owner, "/", repo, "/contents/", path)
params := url.Values{}
if branch != nil {
params.Add("ref", *branch)
}
u = fmt.Sprint(u, "?", params.Encode())
req := pdk.NewHTTPRequest(pdk.MethodGet, u)
req.SetHeader("Authorization", fmt.Sprint("token ", apiKey))
req.SetHeader("Accept", "application/vnd.github.v3+json")
req.SetHeader("User-Agent", "github-mcpx-servlet")
resp := req.Send()
if resp.Status() != 200 {
return UnionContent{}, fmt.Errorf("Failed to get file contents: %d %s (%s)", resp.Status(), string(resp.Body()), u)
}
// attempt to parse this as a file
uc := UnionContent{}
fc := &uc.FileContent
if err := json.Unmarshal(resp.Body(), fc); err == nil {
base64.StdEncoding.DecodeString(fc.Content)
// replace it with the decoded content
fc.Content = string(fc.Content)
return uc, nil
} else {
// if it's not a file, try to parse it as a directory
d := []DirectoryContent{}
if err := json.Unmarshal(resp.Body(), &d); err != nil {
return UnionContent{}, fmt.Errorf("Failed to unmarshal directory contents: %w", err)
}
uc.DirectoryContents = d
uc.isArray = true
return uc, nil
}
}
type FileOperation struct {
Path string `json:"path"`
Content string `json:"content"`
}
func filePushFromArgs(args map[string]interface{}) []FileOperation {
files := []FileOperation{}
if f, ok := args["files"].([]interface{}); ok {
for _, file := range f {
if file, ok := file.(map[string]interface{}); ok {
files = append(files, FileOperation{
Path: file["path"].(string),
Content: file["content"].(string),
})
}
}
}
return files
}
func filesPush(apiKey, owner, repo, branch, message string, files []FileOperation) CallToolResult {
url := fmt.Sprint("https://api.github.com/repos/", owner, "/", repo, "/git/refs/heads/", branch)
req := pdk.NewHTTPRequest(pdk.MethodGet, url)
req.SetHeader("Authorization", fmt.Sprint("token ", apiKey))
req.SetHeader("Accept", "application/vnd.github.v3+json")
req.SetHeader("User-Agent", "github-mcpx-servlet")
resp := req.Send()
if resp.Status() != 200 {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(fmt.Sprint("Failed to get branch: ", resp.Status())),
}},
}
}
ref := RefSchema{}
json.Unmarshal(resp.Body(), &ref)
commitSha := ref.Object.Sha
if tree, err := createTree(apiKey, owner, repo, files, commitSha); err != nil {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(fmt.Sprint("Failed to create tree: ", err)),
}},
}
} else if commit, err := createCommit(apiKey, owner, repo, message, tree.Sha, []string{commitSha}); err != nil {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(fmt.Sprint("Failed to create commit: ", err)),
}},
}
} else {
return updateRef(apiKey, owner, repo, "heads/"+branch, commit.Sha)
}
}
type TreeSchema struct {
BaseTree string `json:"base_tree,omitempty"`
Tree []TreeEntry `json:"tree"`
Truncated bool `json:"truncated,omitempty"`
Url string `json:"url,omitempty"`
Sha string `json:"sha,omitempty"`
}
type TreeEntry struct {
Path string `json:"path"`
Mode string `json:"mode"`
Type string `json:"type"`
Content string `json:"content,omitempty"`
Size int `json:"size,omitempty"`
Sha string `json:"sha,omitempty"`
Url string `json:"url,omitempty"`
}
func createTree(apiKey, owner, repo string, files []FileOperation, baseTree string) (TreeSchema, error) {
tree := TreeSchema{
BaseTree: baseTree,
Tree: []TreeEntry{},
}
for _, file := range files {
tree.Tree = append(tree.Tree, TreeEntry{
Path: file.Path, Mode: "100644", Type: "blob", Content: file.Content})
}
url := fmt.Sprint("https://api.github.com/repos/", owner, "/", repo, "/git/trees")
req := pdk.NewHTTPRequest(pdk.MethodPost, url)
req.SetHeader("Authorization", fmt.Sprint("token ", apiKey))
req.SetHeader("Accept", "application/vnd.github.v3+json")
req.SetHeader("User-Agent", "github-mcpx-servlet")
req.SetHeader("Content-Type", "application/json")
res, err := json.Marshal(tree)
if err != nil {
return TreeSchema{}, fmt.Errorf("Failed to marshal tree: %w", err)
}
req.SetBody(res)
resp := req.Send()
if resp.Status() != 201 {
return TreeSchema{}, fmt.Errorf("Failed to create tree: %d %s", resp.Status(), string(resp.Body()))
}
ts := TreeSchema{}
err = json.Unmarshal(resp.Body(), &res)
return ts, err
}
type Author struct {
Name string `json:"name"`
Email string `json:"email"`
Date string `json:"date"`
}
type Commit struct {
Sha string `json:"sha"`
NodeID string `json:"node_id"`
Url string `json:"url"`
Author Author `json:"author"`
Committer Author `json:"committer"`
Message string `json:"message"`
Tree []struct {
Sha string `json:"sha"`
Url string `json:"url"`
} `json:"tree"`
Parents []struct {
Sha string `json:"sha"`
Url string `json:"url"`
} `json:"parents"`
}
func createCommit(apiKey, owner, repo, message, tree string, parents []string) (Commit, error) {
commit := map[string]interface{}{
"message": message,
"tree": tree,
"parents": parents,
}
url := fmt.Sprint("https://api.github.com/repos/", owner, "/", repo, "/git/commits")
req := pdk.NewHTTPRequest(pdk.MethodPost, url)
req.SetHeader("Authorization", fmt.Sprint("token ", apiKey))
req.SetHeader("Accept", "application/vnd.github.v3+json")
req.SetHeader("User-Agent", "github-mcpx-servlet")
req.SetHeader("Content-Type", "application/json")
res, _ := json.Marshal(commit)
req.SetBody(res)
resp := req.Send()
if resp.Status() != 201 {
return Commit{}, fmt.Errorf("Failed to create commit: %d %s", resp.Status(), string(resp.Body()))
}
cs := Commit{}
json.Unmarshal(resp.Body(), &cs)
return cs, nil
}
func updateRef(apiKey, owner, repo, ref, sha string) CallToolResult {
url := fmt.Sprint("https://api.github.com/repos/", owner, "/", repo, "/git/refs/", ref)
req := pdk.NewHTTPRequest(pdk.MethodPatch, url)
req.SetHeader("Authorization", fmt.Sprint("token ", apiKey))
req.SetHeader("Accept", "application/vnd.github.v3+json")
req.SetHeader("User-Agent", "github-mcpx-servlet")
req.SetHeader("Content-Type", "application/json")
res, _ := json.Marshal(map[string]any{"sha": sha, "force": true})
req.SetBody(res)
resp := req.Send()
if resp.Status() != 200 {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(fmt.Sprint("Failed to update ref: ", resp.Status(), " ", string(resp.Body()))),
}},
}
}
return CallToolResult{
Content: []Content{{
Type: ContentTypeText,
Text: some(string(resp.Body())),
}},
}
}
```
--------------------------------------------------------------------------------
/src/wasm/oci.rs:
--------------------------------------------------------------------------------
```rust
use crate::config::{OciConfig, PluginName};
use anyhow::{Result, anyhow};
use docker_credential::{CredentialRetrievalError, DockerCredential};
use flate2::read::GzDecoder;
use oci_client::{
Client, Reference, client::ClientConfig, manifest, manifest::OciDescriptor,
secrets::RegistryAuth,
};
use sha2::{Digest, Sha256};
use sigstore::{
cosign::{
ClientBuilder, CosignCapabilities,
verification_constraint::{
CertSubjectEmailVerifier, CertSubjectUrlVerifier, VerificationConstraintVec,
cert_subject_email_verifier::StringVerifier,
},
verify_constraints,
},
errors::SigstoreVerifyConstraintsError,
registry::{Auth, OciReference},
trust::{ManualTrustRoot, TrustRoot, sigstore::SigstoreTrustRoot},
};
use std::{fs, io::Read, path::Path, str::FromStr};
use tar::Archive;
use tokio::sync::OnceCell;
use url::Url;
static OCI_CLIENT: OnceCell<Client> = OnceCell::const_new();
fn build_auth(reference: &Reference) -> RegistryAuth {
let server = reference
.resolve_registry()
.strip_suffix('/')
.unwrap_or_else(|| reference.resolve_registry());
// if cli.anonymous {
// return RegistryAuth::Anonymous;
// }
match docker_credential::get_credential(server) {
Err(CredentialRetrievalError::ConfigNotFound) => RegistryAuth::Anonymous,
Err(CredentialRetrievalError::NoCredentialConfigured) => RegistryAuth::Anonymous,
Err(e) => {
tracing::info!("Error retrieving docker credentials: {e}. Using anonymous auth");
RegistryAuth::Anonymous
}
Ok(DockerCredential::UsernamePassword(username, password)) => {
tracing::info!("Found docker credentials");
RegistryAuth::Basic(username, password)
}
Ok(DockerCredential::IdentityToken(_)) => {
tracing::info!(
"Cannot use contents of docker config, identity token not supported. Using anonymous auth"
);
RegistryAuth::Anonymous
}
}
}
pub async fn load_wasm(url: &Url, config: &OciConfig, plugin_name: &PluginName) -> Result<Vec<u8>> {
let image_reference = url.as_str().strip_prefix("oci://").unwrap();
let target_file_path = "/plugin.wasm";
let mut hasher = Sha256::new();
hasher.update(image_reference);
let hash = hasher.finalize();
let short_hash = &hex::encode(hash)[..7];
let cache_dir = dirs::cache_dir()
.map(|mut path| {
path.push("hyper-mcp");
path
})
.unwrap();
std::fs::create_dir_all(&cache_dir)?;
let local_output_path = cache_dir.join(format!("{plugin_name}-{short_hash}.wasm"));
let local_output_path = local_output_path.to_str().unwrap();
if let Err(e) =
pull_and_extract_oci_image(config, image_reference, target_file_path, local_output_path)
.await
{
tracing::error!("Error pulling oci plugin: {e}");
return Err(anyhow::anyhow!("Failed to pull OCI plugin: {e}"));
}
tracing::info!("cache plugin `{plugin_name}` to : {local_output_path}");
tokio::fs::read(local_output_path)
.await
.map_err(|e| e.into())
}
async fn setup_trust_repository(config: &OciConfig) -> Result<Box<dyn TrustRoot>> {
if config.use_sigstore_tuf_data {
// Use Sigstore TUF data from the official repository
tracing::info!("Using Sigstore TUF data for verification");
match SigstoreTrustRoot::new(None).await {
Ok(repo) => return Ok(Box::new(repo)),
Err(e) => {
tracing::error!("Failed to initialize TUF trust repository: {e}");
if !config.insecure_skip_signature {
return Err(anyhow!(
"Failed to initialize TUF trust repository and signature verification is required"
));
}
tracing::info!("Falling back to manual trust repository");
}
}
}
// Create a manual trust repository
let mut data = ManualTrustRoot::default();
// Add Rekor public keys if provided
if let Some(rekor_keys_path) = &config.rekor_pub_keys {
if rekor_keys_path.exists() {
match fs::read(rekor_keys_path) {
Ok(content) => {
tracing::info!("Added Rekor public key");
if let Some(path_str) = rekor_keys_path.to_str() {
data.rekor_keys.insert(path_str.to_string(), content);
tracing::info!("Added Rekor public key from: {}", path_str);
}
}
Err(e) => tracing::warn!("Failed to read Rekor public keys file: {e}"),
}
} else {
tracing::warn!("Rekor public keys file not found: {rekor_keys_path:?}");
}
}
// Add Fulcio certificates if provided
if let Some(fulcio_certs_path) = &config.fulcio_certs {
if fulcio_certs_path.exists() {
match fs::read(fulcio_certs_path) {
Ok(content) => {
let certificate = sigstore::registry::Certificate {
encoding: sigstore::registry::CertificateEncoding::Pem,
data: content,
};
match certificate.try_into() {
Ok(cert) => {
tracing::info!("Added Fulcio certificate");
data.fulcio_certs.push(cert);
}
Err(e) => tracing::warn!("Failed to parse Fulcio certificate: {e}"),
}
}
Err(e) => tracing::warn!("Failed to read Fulcio certificates file: {e}"),
}
} else {
tracing::warn!("Fulcio certificates file not found: {fulcio_certs_path:?}");
}
}
Ok(Box::new(data))
}
async fn verify_image_signature(config: &OciConfig, image_reference: &str) -> Result<bool> {
tracing::info!("Verifying signature for {image_reference}");
// Set up the trust repository based on CLI arguments
let repo = setup_trust_repository(config).await?;
let auth = &Auth::Anonymous;
// Create a client builder
let client_builder = ClientBuilder::default();
// Create client with trust repository
let client_builder = match client_builder.with_trust_repository(repo.as_ref()) {
Ok(builder) => builder,
Err(e) => return Err(anyhow!("Failed to set up trust repository: {e}")),
};
// Build the client
let mut client = match client_builder.build() {
Ok(client) => client,
Err(e) => return Err(anyhow!("Failed to build Sigstore client: {e}")),
};
// Parse the reference
let image_ref = match OciReference::from_str(image_reference) {
Ok(reference) => reference,
Err(e) => return Err(anyhow!("Invalid image reference: {e}")),
};
// Triangulate to find the signature image and source digest
let (cosign_signature_image, source_image_digest) =
match client.triangulate(&image_ref, auth).await {
Ok((sig_image, digest)) => (sig_image, digest),
Err(e) => {
tracing::warn!("Failed to triangulate image: {e}");
return Ok(false); // No signatures found
}
};
// Get trusted signature layers
let signature_layers = match client
.trusted_signature_layers(auth, &source_image_digest, &cosign_signature_image)
.await
{
Ok(layers) => layers,
Err(e) => {
tracing::warn!("Failed to get trusted signature layers: {e}");
return Ok(false);
}
};
if signature_layers.is_empty() {
tracing::warn!("No valid signatures found for {image_reference}");
return Ok(false);
}
// Build verification constraints based on CLI options
let mut verification_constraints: VerificationConstraintVec = Vec::new();
if let Some(cert_email) = &config.cert_email {
let issuer = config
.cert_issuer
.as_ref()
.map(|i| StringVerifier::ExactMatch(i.to_string()));
verification_constraints.push(Box::new(CertSubjectEmailVerifier {
email: StringVerifier::ExactMatch(cert_email.to_string()),
issuer,
}));
}
if let Some(cert_url) = &config.cert_url {
match config.cert_issuer.as_ref() {
Some(issuer) => {
verification_constraints.push(Box::new(CertSubjectUrlVerifier {
url: cert_url.to_string(),
issuer: issuer.to_string(),
}));
}
None => {
tracing::warn!("'cert-issuer' is required when 'cert-url' is specified");
}
}
}
// Verify the constraints
match verify_constraints(&signature_layers, verification_constraints.iter()) {
Ok(()) => {
tracing::info!("Signature verification successful for {image_reference}");
Ok(true)
}
Err(SigstoreVerifyConstraintsError {
unsatisfied_constraints,
}) => {
tracing::warn!(
"Signature verification failed for {image_reference}: {unsatisfied_constraints:?}"
);
Ok(false)
}
}
}
async fn pull_and_extract_oci_image(
config: &OciConfig,
image_reference: &str,
target_file_path: &str,
local_output_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
if Path::new(local_output_path).exists() {
tracing::info!(
"Plugin {image_reference} already cached at: {local_output_path}. Skipping downloading."
);
return Ok(());
}
tracing::info!("Pulling {image_reference} ...");
let reference = Reference::try_from(image_reference)?;
let auth = build_auth(&reference);
// Verify the image signature if it's an OCI image and verification is enabled
if !config.insecure_skip_signature {
tracing::info!("Signature verification enabled for {image_reference}");
match verify_image_signature(config, image_reference).await {
Ok(verified) => {
if !verified {
return Err(format!(
"No valid signatures found for the image {image_reference}"
)
.into());
}
}
Err(e) => {
return Err(format!("Image signature verification failed: {e}").into());
}
}
} else {
tracing::warn!("Signature verification disabled for {image_reference}");
}
let client = OCI_CLIENT
.get_or_init(|| async { Client::new(ClientConfig::default()) })
.await;
// Accept both OCI and Docker manifest types
let manifest = client
.pull(
&reference,
&auth,
vec![
manifest::IMAGE_MANIFEST_MEDIA_TYPE,
manifest::IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE,
manifest::IMAGE_LAYER_GZIP_MEDIA_TYPE,
],
)
.await?;
for layer in manifest.layers.iter() {
let mut buf = Vec::new();
let desc = OciDescriptor {
digest: layer.sha256_digest().clone(),
media_type: "application/vnd.docker.image.rootfs.diff.tar.gzip".to_string(),
..Default::default()
};
client.pull_blob(&reference, &desc, &mut buf).await.unwrap();
let gz_extract = GzDecoder::new(&buf[..]);
let mut archive_extract = Archive::new(gz_extract);
for entry_result in archive_extract.entries()? {
match entry_result {
Ok(mut entry) => {
if let Ok(path) = entry.path() {
let path_str = path.to_string_lossy();
if path_str.ends_with(target_file_path) || path_str.ends_with("plugin.wasm")
{
if let Some(parent) = Path::new(local_output_path).parent() {
fs::create_dir_all(parent)?;
}
let mut content = Vec::new();
entry.read_to_end(&mut content)?;
fs::write(local_output_path, content)?;
tracing::info!("Successfully extracted to: {local_output_path}");
return Ok(());
}
}
}
Err(e) => tracing::info!("Error during extraction: {e}"),
}
}
}
Err("Target file not found in any layer".into())
}
```
--------------------------------------------------------------------------------
/src/plugin.rs:
--------------------------------------------------------------------------------
```rust
use crate::config::PluginName;
use async_trait::async_trait;
use rmcp::{
ErrorData as McpError,
model::*,
service::{NotificationContext, RequestContext, RoleServer},
};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::{Value, json};
use std::{
fmt::Debug,
ops::Deref,
sync::{Arc, Mutex},
};
use tokio_util::sync::CancellationToken;
type PluginHandle = Arc<Mutex<extism::Plugin>>;
#[derive(Clone, Debug, Serialize, Deserialize)]
struct PluginRequestContext {
pub id: NumberOrString,
#[serde(rename = "_meta")]
pub meta: Meta,
}
impl<'a> From<&'a RequestContext<RoleServer>> for PluginRequestContext {
fn from(context: &'a RequestContext<RoleServer>) -> Self {
PluginRequestContext {
id: context.id.clone(),
meta: context.meta.clone(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct PluginNotificationContext {
#[serde(rename = "_meta")]
pub meta: Meta,
}
impl<'a> From<&'a NotificationContext<RoleServer>> for PluginNotificationContext {
fn from(context: &'a NotificationContext<RoleServer>) -> Self {
PluginNotificationContext {
meta: context.meta.clone(),
}
}
}
#[async_trait]
#[allow(unused_variables)]
pub trait Plugin: Send + Sync + Debug {
async fn call_tool(
&self,
request: CallToolRequestParam,
context: RequestContext<RoleServer>,
) -> Result<CallToolResult, McpError>;
async fn complete(
&self,
request: CompleteRequestParam,
context: RequestContext<RoleServer>,
) -> Result<CompleteResult, McpError> {
Ok(CompleteResult::default())
}
async fn get_prompt(
&self,
request: GetPromptRequestParam,
context: RequestContext<RoleServer>,
) -> Result<GetPromptResult, McpError> {
Err(McpError::method_not_found::<GetPromptRequestMethod>())
}
async fn list_prompts(
&self,
request: Option<PaginatedRequestParam>,
context: RequestContext<RoleServer>,
) -> Result<ListPromptsResult, McpError> {
Ok(ListPromptsResult::default())
}
async fn list_resources(
&self,
request: Option<PaginatedRequestParam>,
context: RequestContext<RoleServer>,
) -> Result<ListResourcesResult, McpError> {
Ok(ListResourcesResult::default())
}
async fn list_resource_templates(
&self,
request: Option<PaginatedRequestParam>,
context: RequestContext<RoleServer>,
) -> Result<ListResourceTemplatesResult, McpError> {
Ok(ListResourceTemplatesResult::default())
}
async fn list_tools(
&self,
request: Option<PaginatedRequestParam>,
context: RequestContext<RoleServer>,
) -> Result<ListToolsResult, McpError>;
fn name(&self) -> &PluginName;
async fn on_roots_list_changed(
&self,
context: NotificationContext<RoleServer>,
) -> Result<(), McpError> {
Ok(())
}
fn plugin(&self) -> &PluginHandle;
async fn read_resource(
&self,
request: ReadResourceRequestParam,
context: RequestContext<RoleServer>,
) -> Result<ReadResourceResult, McpError> {
Err(McpError::method_not_found::<ReadResourceRequestMethod>())
}
}
async fn call_plugin<R>(
plugin: &dyn Plugin,
name: &str,
payload: String,
ct: CancellationToken,
) -> Result<R, McpError>
where
R: DeserializeOwned + Send + 'static,
{
let plugin_name = plugin.name().to_string();
if !function_exists_plugin(plugin, name) {
return Err(McpError::invalid_request(
format!("Method {name} not found for plugin {plugin_name}"),
None,
));
}
let plugin = Arc::clone(plugin.plugin());
let cancel_handle = {
let guard = plugin.lock().unwrap();
guard.cancel_handle()
};
let name = name.to_string();
let mut join = tokio::task::spawn_blocking(move || {
let mut plugin = plugin.lock().unwrap();
let result: Result<String, extism::Error> = plugin.call(&name, payload);
match result {
Ok(res) => match serde_json::from_str::<R>(&res) {
Ok(parsed) => Ok(parsed),
Err(e) => Err(McpError::internal_error(
format!("Failed to deserialize data: {e}"),
None,
)),
},
Err(e) => Err(McpError::internal_error(
format!("Failed to call plugin: {e}"),
None,
)),
}
});
tokio::select! {
// Finished normally
res = &mut join => {
match res {
Ok(Ok(result)) => Ok(result),
Ok(Err(e)) => Err(e),
Err(e) => Err(McpError::internal_error(
format!("Failed to spawn blocking task for plugin {plugin_name}: {e}"),
None,
)),
}
}
//Cancellation requested
_ = ct.cancelled() => {
if let Err(e) = cancel_handle.cancel() {
tracing::error!("Failed to cancel plugin {plugin_name}: {e}");
return Err(McpError::internal_error(
format!("Failed to cancel plugin {plugin_name}: {e}"),
None,
));
}
match tokio::time::timeout(std::time::Duration::from_millis(250), join).await {
Ok(Ok(Ok(_))) => Err(McpError::internal_error(
format!("Plugin {plugin_name} was cancelled"),
None,
)),
Ok(Ok(Err(e))) => Err(McpError::internal_error(
format!("Failed to execute plugin {plugin_name}: {e}"),
None,
)),
Ok(Err(e)) => Err(McpError::internal_error(
format!("Join error for plugin {plugin_name}: {e}"),
None,
)),
Err(_) => Err(McpError::internal_error(
format!("Timeout waiting for plugin {plugin_name} to cancel"),
None,
)),
}
}
}
}
fn function_exists_plugin(plugin: &dyn Plugin, name: &str) -> bool {
let plugin = Arc::clone(plugin.plugin());
plugin.lock().unwrap().function_exists(name)
}
async fn notify_plugin(plugin: &dyn Plugin, name: &str, payload: String) -> Result<(), McpError> {
let plugin_name = plugin.name().to_string();
if !function_exists_plugin(plugin, name) {
return Err(McpError::invalid_request(
format!("Method {name} not found for plugin {plugin_name}"),
None,
));
}
let plugin = Arc::clone(plugin.plugin());
let name = name.to_string();
tokio::task::spawn_blocking(move || {
let mut plugin = plugin.lock().unwrap();
let result: Result<String, extism::Error> = plugin.call(&name, payload);
if let Err(e) = result {
tracing::error!("Failed to notify plugin {plugin_name}: {e}");
}
});
Ok(())
}
#[derive(Debug)]
pub struct PluginBase {
pub name: PluginName,
pub plugin: PluginHandle,
}
#[derive(Debug)]
pub struct PluginV1(pub PluginBase);
impl Deref for PluginV1 {
type Target = PluginBase;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl Plugin for PluginV1 {
async fn call_tool(
&self,
request: CallToolRequestParam,
context: RequestContext<RoleServer>,
) -> Result<CallToolResult, McpError> {
call_plugin::<CallToolResult>(
self,
"call",
serde_json::to_string(&json!({
"params": request,
}))
.expect("Failed to serialize request"),
context.ct,
)
.await
}
async fn list_tools(
&self,
_request: Option<PaginatedRequestParam>,
context: RequestContext<RoleServer>,
) -> Result<ListToolsResult, McpError> {
call_plugin::<ListToolsResult>(self, "describe", "".to_string(), context.ct).await
}
fn name(&self) -> &PluginName {
&self.name
}
fn plugin(&self) -> &PluginHandle {
&self.plugin
}
}
impl PluginV1 {
pub fn new(name: PluginName, plugin: PluginHandle) -> Self {
Self(PluginBase { name, plugin })
}
}
#[derive(Debug)]
pub struct PluginV2(pub PluginBase);
impl Deref for PluginV2 {
type Target = PluginBase;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl Plugin for PluginV2 {
async fn call_tool(
&self,
request: CallToolRequestParam,
context: RequestContext<RoleServer>,
) -> Result<CallToolResult, McpError> {
call_plugin::<CallToolResult>(
self,
"call_tool",
serde_json::to_string(&json!({
"request": request,
"context": PluginRequestContext::from(&context),
}))
.expect("Failed to serialize request"),
context.ct,
)
.await
}
async fn complete(
&self,
request: CompleteRequestParam,
context: RequestContext<RoleServer>,
) -> Result<CompleteResult, McpError> {
#[derive(Debug, Clone)]
struct Helper(CompleteRequestParam);
impl Serialize for Helper {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut value = serde_json::to_value(&self.0).map_err(serde::ser::Error::custom)?;
if let Value::Object(root) = &mut value
&& let Some(Value::Object(ref_obj)) = root.get_mut("ref")
&& let Some(Value::String(t)) = ref_obj.get_mut("type")
&& let Some(stripped) = t.strip_prefix("ref/")
{
*t = stripped.to_string();
}
value.serialize(serializer)
}
}
call_plugin::<CompleteResult>(
self,
"complete",
serde_json::to_string(&json!({
"request": Helper(request),
"context": PluginRequestContext::from(&context),
}))
.expect("Failed to serialize request"),
context.ct,
)
.await
}
async fn get_prompt(
&self,
request: GetPromptRequestParam,
context: RequestContext<RoleServer>,
) -> Result<GetPromptResult, McpError> {
call_plugin::<GetPromptResult>(
self,
"get_prompt",
serde_json::to_string(&json!({
"request": request,
"context": PluginRequestContext::from(&context),
}))
.expect("Failed to serialize request"),
context.ct,
)
.await
}
async fn list_prompts(
&self,
_request: Option<PaginatedRequestParam>,
context: RequestContext<RoleServer>,
) -> Result<ListPromptsResult, McpError> {
if !function_exists_plugin(self, "list_prompts") {
return Ok(ListPromptsResult::default());
}
call_plugin::<ListPromptsResult>(
self,
"list_prompts",
serde_json::to_string(&json!({
"context": PluginRequestContext::from(&context),
}))
.expect("Failed to serialize context"),
context.ct,
)
.await
}
async fn list_resources(
&self,
_request: Option<PaginatedRequestParam>,
context: RequestContext<RoleServer>,
) -> Result<ListResourcesResult, McpError> {
if !function_exists_plugin(self, "list_resources") {
return Ok(ListResourcesResult::default());
}
call_plugin::<ListResourcesResult>(
self,
"list_resources",
serde_json::to_string(&json!({
"context": PluginRequestContext::from(&context),
}))
.expect("Failed to serialize context"),
context.ct,
)
.await
}
async fn list_resource_templates(
&self,
_request: Option<PaginatedRequestParam>,
context: RequestContext<RoleServer>,
) -> Result<ListResourceTemplatesResult, McpError> {
if !function_exists_plugin(self, "list_resource_templates") {
return Ok(ListResourceTemplatesResult::default());
}
call_plugin::<ListResourceTemplatesResult>(
self,
"list_resource_templates",
serde_json::to_string(&json!({
"context": PluginRequestContext::from(&context),
}))
.expect("Failed to serialize context"),
context.ct,
)
.await
}
async fn list_tools(
&self,
_request: Option<PaginatedRequestParam>,
context: RequestContext<RoleServer>,
) -> Result<ListToolsResult, McpError> {
if !function_exists_plugin(self, "list_tools") {
return Ok(ListToolsResult::default());
}
call_plugin::<ListToolsResult>(
self,
"list_tools",
serde_json::to_string(&json!({
"context": PluginRequestContext::from(&context),
}))
.expect("Failed to serialize context"),
context.ct,
)
.await
}
fn name(&self) -> &PluginName {
&self.name
}
async fn on_roots_list_changed(
&self,
context: NotificationContext<RoleServer>,
) -> Result<(), McpError> {
if function_exists_plugin(self, "on_roots_list_changed") {
return Ok(());
}
notify_plugin(
self,
"on_roots_list_changed",
serde_json::to_string(&json!({
"context": PluginNotificationContext::from(&context),
}))
.expect("Failed to serialize context"),
)
.await
}
fn plugin(&self) -> &PluginHandle {
&self.plugin
}
async fn read_resource(
&self,
request: ReadResourceRequestParam,
context: RequestContext<RoleServer>,
) -> Result<ReadResourceResult, McpError> {
call_plugin::<ReadResourceResult>(
self,
"read_resource",
serde_json::to_string(&json!({
"request": request,
"context": PluginRequestContext::from(&context),
}))
.expect("Failed to serialize request"),
context.ct,
)
.await
}
}
impl PluginV2 {
pub fn new(name: PluginName, plugin: PluginHandle) -> Self {
Self(PluginBase { name, plugin })
}
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/context7/src/lib.rs:
--------------------------------------------------------------------------------
```rust
mod pdk;
use extism_pdk::*;
use pdk::types::{
CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
};
use serde_json::{Value as JsonValue, json};
use urlencoding::encode;
const CONTEXT7_API_BASE_URL: &str = "https://context7.com/api"; // Guessed API base URL
pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
match input.params.name.as_str() {
"c7_resolve_library_id" => c7_resolve_library_id(input),
"c7_get_library_docs" => c7_get_library_docs(input),
_ => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Unknown tool: {}", input.params.name)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
}
fn c7_resolve_library_id(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.unwrap_or_default();
let library_name_val = args.get("library_name").unwrap_or(&JsonValue::Null);
if let JsonValue::String(library_name_as_query) = library_name_val {
let encoded_query = encode(library_name_as_query);
let url = format!(
"{}/v1/search?query={}",
CONTEXT7_API_BASE_URL, encoded_query
);
let mut req = HttpRequest::new(&url).with_method("GET");
req.headers
.insert("X-Context7-Source".to_string(), "mcp-server".to_string());
match http::request::<()>(&req, None) {
Ok(res) => {
let body_str = String::from_utf8_lossy(&res.body()).to_string();
if res.status_code() >= 200 && res.status_code() < 300 {
match serde_json::from_str::<JsonValue>(&body_str) {
Ok(parsed_json) => {
let mut results_text_parts = Vec::new();
// Check if the root is an object and has a "results" field which is an array
if let Some(results_node) = parsed_json.get("results") {
if let JsonValue::Array(results_array) = results_node {
if results_array.is_empty() {
results_text_parts.push(
"No libraries found matching your query.".to_string(),
);
} else {
for result_item in results_array {
let mut item_details = Vec::new();
let title = result_item
.get("title")
.and_then(JsonValue::as_str)
.unwrap_or("N/A");
item_details.push(format!("- Title: {}", title));
let id = result_item
.get("id")
.and_then(JsonValue::as_str)
.unwrap_or("N/A");
item_details.push(format!(
"- Context7-compatible library ID: {}",
id
));
let description = result_item
.get("description")
.and_then(JsonValue::as_str)
.unwrap_or("N/A");
item_details
.push(format!("- Description: {}", description));
if let Some(v) = result_item
.get("totalSnippets")
.and_then(JsonValue::as_i64)
.filter(|&v| v >= 0)
{
item_details.push(format!("- Code Snippets: {}", v))
}
if let Some(v) = result_item
.get("stars")
.and_then(JsonValue::as_i64)
.filter(|&v| v >= 0)
{
item_details.push(format!("- GitHub Stars: {}", v))
}
results_text_parts.push(item_details.join("\n"));
}
}
} else {
results_text_parts.push("API response 'results' field was not an array as expected.".to_string());
}
} else {
results_text_parts.push(
"API response did not contain a 'results' field as expected."
.to_string(),
);
}
let header = "Available Libraries (top matches):\n\nEach result includes information like:\n- Title: Library or package name\n- Context7-compatible library ID: Identifier (format: /org/repo)\n- Description: Short summary\n- Code Snippets: Number of available code examples (if available)\n- GitHub Stars: Popularity indicator (if available)\n\nFor best results, select libraries based on name match, popularity (stars), snippet coverage, and relevance to your use case.\n\n---\n";
let final_text =
format!("{}{}", header, results_text_parts.join("\n\n"));
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(final_text),
mime_type: Some("text/markdown".to_string()),
r#type: ContentType::Text,
data: None,
}],
})
}
Err(e) => {
// Failed to parse the JSON body
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!(
"Failed to parse API response JSON: {}. Body: {}",
e, body_str
)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!(
"API request failed with status {}: {}",
res.status_code(),
body_str
)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
Err(e) => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("HTTP request failed: {}", e)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(
"Missing required parameter: library_name (or not a string)".to_string(),
),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
fn c7_get_library_docs(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.unwrap_or_default();
let library_id_json_val = args
.get("context7_compatible_library_id")
.unwrap_or(&JsonValue::Null);
if let JsonValue::String(original_id_str) = library_id_json_val {
let mut id_for_path = original_id_str.clone();
let mut folders_value_opt: Option<String> = None;
if let Some(idx) = original_id_str.rfind("?folders=") {
let (id_part, folders_part_with_query) = original_id_str.split_at(idx);
id_for_path = id_part.to_string();
folders_value_opt = Some(
folders_part_with_query
.trim_start_matches("?folders=")
.to_string(),
);
}
let mut query_params_vec = vec![format!(
"context7CompatibleLibraryID={}",
encode(original_id_str) // Use the original, full ID string for this query parameter
)];
if let Some(folders_val) = &folders_value_opt {
if !folders_val.is_empty() {
query_params_vec.push(format!("folders={}", encode(folders_val)));
}
}
if let Some(JsonValue::String(topic_str)) = args.get("topic") {
if !topic_str.is_empty() {
// Ensure topic is not empty before adding
query_params_vec.push(format!("topic={}", encode(topic_str)));
}
}
if let Some(JsonValue::Number(tokens_num_json)) = args.get("tokens") {
if let Some(tokens_f64) = tokens_num_json.as_f64() {
query_params_vec.push(format!("tokens={}", tokens_f64 as i64));
} else if let Some(tokens_i64) = tokens_num_json.as_i64() {
query_params_vec.push(format!("tokens={}", tokens_i64));
}
}
let final_id_for_path_segment = id_for_path.strip_prefix("/").unwrap_or(&id_for_path);
let query_params = query_params_vec.join("&");
let url = format!(
"{}/v1/{}/?{}", // Corrected URL: ensure '?' before query parameters
CONTEXT7_API_BASE_URL, final_id_for_path_segment, query_params
);
let mut req = HttpRequest::new(&url).with_method("GET");
req.headers
.insert("X-Context7-Source".to_string(), "mcp-server".to_string());
match http::request::<()>(&req, None) {
Ok(res) => {
let body_str = String::from_utf8_lossy(&res.body()).to_string();
if res.status_code() >= 200 && res.status_code() < 300 {
// Directly use the body_str as markdown content
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(body_str),
mime_type: Some("text/markdown".to_string()), // Assuming it's still markdown
r#type: ContentType::Text,
data: None,
}],
})
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!(
"API request for docs (URL: {}) failed with status {}: {}",
url,
res.status_code(),
body_str
)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
Err(e) => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("HTTP request for docs failed: {}, URL: {}", e, url)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(
"Missing required parameter: context7_compatible_library_id (or not a string)"
.to_string(),
),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
pub(crate) fn describe() -> Result<ListToolsResult, Error> {
Ok(ListToolsResult {
tools: vec![
ToolDescription {
name: "c7_resolve_library_id".into(),
description: "Resolves a package name to a Context7-compatible library ID and returns a list of matching libraries. You MUST call this function before 'c7_get_library_docs' to obtain a valid Context7-compatible library ID. When selecting the best match, consider: - Name similarity to the query - Description relevance - Code Snippet count (documentation coverage) - GitHub Stars (popularity) Return the selected library ID and explain your choice. If there are multiple good matches, mention this but proceed with the most relevant one.".into(),
input_schema: json!({
"type": "object",
"properties": {
"library_name": {
"type": "string",
"description": "Library name to search for and retrieve a Context7-compatible library ID.",
},
},
"required": ["library_name"],
})
.as_object()
.unwrap()
.clone(),
},
ToolDescription {
name: "c7_get_library_docs".into(),
description: "Fetches up-to-date documentation for a library. You must call 'c7_resolve_library_id' first to obtain the exact Context7-compatible library ID required to use this tool.".into(),
input_schema: json!({
"type": "object",
"properties": {
"context7_compatible_library_id": {
"type": "string",
"description": "Exact Context7-compatible library ID (e.g., 'mongodb/docs', 'vercel/nextjs') retrieved from 'c7_resolve_library_id'.",
},
"topic": {
"type": "string",
"description": "Topic to focus documentation on (e.g., 'hooks', 'routing').",
},
"tokens": {
"type": "integer",
"description": "Maximum number of tokens of documentation to retrieve (default: 10000). Higher values provide more context but consume more tokens.",
},
},
"required": ["context7_compatible_library_id"],
})
.as_object()
.unwrap()
.clone(),
},
],
})
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/qdrant/src/qdrant_client.rs:
--------------------------------------------------------------------------------
```rust
use anyhow::{Error, anyhow, bail};
use extism_pdk::*;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::{Map, Value};
use std::collections::BTreeMap;
use std::fmt::Display;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PointId {
Uuid(String),
Num(u64),
}
impl From<u64> for PointId {
fn from(num: u64) -> Self {
PointId::Num(num)
}
}
impl From<String> for PointId {
fn from(uuid: String) -> Self {
PointId::Uuid(uuid)
}
}
impl Display for PointId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PointId::Uuid(uuid) => write!(f, "{}", uuid),
PointId::Num(num) => write!(f, "{}", num),
}
}
}
/// The point struct.
/// A point is a record consisting of a vector and an optional payload.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Point {
/// Id of the point
pub id: PointId,
/// Vectors
pub vector: Vec<f32>,
/// Additional information along with vectors
pub payload: Option<Map<String, Value>>,
}
/// The point struct with the score returned by searching
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScoredPoint {
/// Id of the point
pub id: PointId,
/// Vectors
pub vector: Option<Vec<f32>>,
/// Additional information along with vectors
pub payload: Option<Map<String, Value>>,
/// Points vector distance to the query vector
pub score: f32,
}
pub struct QdrantClient {
url_base: String,
api_key: Option<String>,
}
impl QdrantClient {
pub fn new_with_url(url_base_: String) -> QdrantClient {
QdrantClient {
url_base: url_base_,
api_key: None,
}
}
pub fn new() -> QdrantClient {
QdrantClient::new_with_url("http://localhost:6333".to_string())
}
pub fn set_api_key(&mut self, api_key: impl Into<String>) {
self.api_key = Some(api_key.into());
}
}
impl Default for QdrantClient {
fn default() -> Self {
Self::new()
}
}
/// Shortcut functions
impl QdrantClient {
/// Shortcut functions
pub fn collection_info(&self, collection_name: &str) -> Result<u64, Error> {
let v = self.collection_info_api(collection_name)?;
v.get("result")
.and_then(|v| v.get("points_count"))
.and_then(|v| v.as_u64())
.ok_or_else(|| anyhow!("[qdrant] Invalid response format"))
}
pub fn create_collection(&self, collection_name: &str, size: u32) -> Result<(), Error> {
match self.collection_exists(collection_name)? {
false => (),
true => {
let err_msg = format!("Collection '{}' already exists", collection_name);
bail!(err_msg);
}
}
let params = json!({
"vectors": {
"size": size,
"distance": "Cosine",
"on_disk": true,
}
});
if !self.create_collection_api(collection_name, ¶ms)? {
bail!("Failed to create collection '{}'", collection_name);
}
Ok(())
}
pub fn list_collections(&self) -> Result<Vec<String>, Error> {
self.list_collections_api()
}
pub fn collection_exists(&self, collection_name: &str) -> Result<bool, Error> {
let collection_names = self.list_collections()?;
Ok(collection_names.contains(&collection_name.to_string()))
}
pub fn delete_collection(&self, collection_name: &str) -> Result<(), Error> {
match self.collection_exists(collection_name)? {
true => (),
false => {
let err_msg = format!("Not found collection '{}'", collection_name);
bail!(err_msg);
}
}
if !self.delete_collection_api(collection_name)? {
bail!("Failed to delete collection '{}'", collection_name);
}
Ok(())
}
pub fn upsert_points(&self, collection_name: &str, points: Vec<Point>) -> Result<(), Error> {
let params = json!({
"points": points,
});
self.upsert_points_api(collection_name, ¶ms)
}
pub fn search_points(
&self,
collection_name: &str,
vector: Vec<f32>,
limit: u64,
score_threshold: Option<f32>,
) -> Result<Vec<ScoredPoint>, Error> {
let score_threshold = score_threshold.unwrap_or(0.0);
let params = json!({
"vector": vector,
"limit": limit,
"with_payload": true,
"with_vector": true,
"score_threshold": score_threshold,
});
match self.search_points_api(collection_name, ¶ms) {
Ok(v) => match v.get("result") {
Some(v) => match v.as_array() {
Some(rs) => {
let mut sps: Vec<ScoredPoint> = Vec::<ScoredPoint>::new();
for r in rs {
let sp: ScoredPoint = serde_json::from_value(r.clone())?;
sps.push(sp);
}
Ok(sps)
}
None => {
bail!(
"[qdrant] The value corresponding to the 'result' key is not an array."
)
}
},
None => Ok(vec![]),
},
Err(_) => Ok(vec![]),
}
}
pub fn get_points(&self, collection_name: &str, ids: &[PointId]) -> Result<Vec<Point>, Error> {
let params = json!({
"ids": ids,
"with_payload": true,
"with_vector": true,
});
let v = self.get_points_api(collection_name, ¶ms)?;
let rs = v
.get("result")
.and_then(|v| v.as_array())
.ok_or_else(|| anyhow!("[qdrant] Invalid response format"))?;
let mut ps: Vec<Point> = Vec::new();
for r in rs {
let p: Point = serde_json::from_value(r.clone())?;
ps.push(p);
}
Ok(ps)
}
pub fn get_point(&self, collection_name: &str, id: &PointId) -> Result<Point, Error> {
let v = self.get_point_api(collection_name, id)?;
let r = v
.get("result")
.ok_or_else(|| anyhow!("[qdrant] Invalid response format"))?;
Ok(serde_json::from_value(r.clone())?)
}
pub fn delete_points(&self, collection_name: &str, ids: &[PointId]) -> Result<(), Error> {
let params = json!({
"points": ids,
});
self.delete_points_api(collection_name, ¶ms)
}
/// REST API functions
pub fn collection_info_api(&self, collection_name: &str) -> Result<Value, Error> {
let url = format!("{}/collections/{}", self.url_base, collection_name);
let mut headers = BTreeMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
if let Some(api_key) = &self.api_key {
headers.insert("api-key".to_string(), api_key.clone());
}
let response: HttpResponse = http::request::<()>(
&HttpRequest {
url: url.clone(),
headers,
method: Some("GET".to_string()),
},
None,
)?;
let json: Value = serde_json::from_slice(&response.body())?;
Ok(json)
}
pub fn create_collection_api(
&self,
collection_name: &str,
params: &Value,
) -> Result<bool, Error> {
let url = format!("{}/collections/{}", self.url_base, collection_name);
let mut headers = BTreeMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
if let Some(api_key) = &self.api_key {
headers.insert("api-key".to_string(), api_key.clone());
}
let body = serde_json::to_vec(params)?;
let response = http::request::<Vec<u8>>(
&HttpRequest {
url: url.clone(),
headers,
method: Some("PUT".to_string()),
},
Some(body),
)?;
let json: Value = serde_json::from_slice(&response.body())?;
let success = json
.get("result")
.and_then(|v| v.as_bool())
.ok_or_else(|| anyhow!("[qdrant] Invalid response format"))?;
Ok(success)
}
pub fn list_collections_api(&self) -> Result<Vec<String>, Error> {
let url = format!("{}/collections", self.url_base);
let mut headers = BTreeMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
if let Some(api_key) = &self.api_key {
headers.insert("api-key".to_string(), api_key.clone());
}
let response = http::request::<()>(
&HttpRequest {
url: url.clone(),
headers,
method: Some("GET".to_string()),
},
None,
)?;
let json: Value = serde_json::from_slice(&response.body())?;
match json.get("result") {
Some(result) => match result.get("collections") {
Some(collections) => match collections.as_array() {
Some(collections) => {
let mut collection_names = Vec::new();
for collection in collections {
if let Some(name) = collection.get("name").and_then(|n| n.as_str()) {
collection_names.push(name.to_string());
}
}
Ok(collection_names)
}
None => bail!(
"[qdrant] The value corresponding to the 'collections' key is not an array."
),
},
None => bail!("[qdrant] The given key 'collections' does not exist."),
},
None => bail!("[qdrant] The given key 'result' does not exist."),
}
}
pub fn collection_exists_api(&self, collection_name: &str) -> Result<bool, Error> {
let url = format!("{}/collections/{}/exists", self.url_base, collection_name);
let mut headers = BTreeMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
if let Some(api_key) = &self.api_key {
headers.insert("api-key".to_string(), api_key.clone());
}
let response = http::request::<()>(
&HttpRequest {
url: url.clone(),
headers,
method: Some("GET".to_string()),
},
None,
)?;
let json: Value = serde_json::from_slice(&response.body())?;
match json.get("result") {
Some(result) => {
let exists = result
.get("exists")
.and_then(|v| v.as_bool())
.ok_or_else(|| anyhow!("[qdrant] Invalid response format"))?;
Ok(exists)
}
None => Err(anyhow!("[qdrant] Failed to check collection existence")),
}
}
pub fn delete_collection_api(&self, collection_name: &str) -> Result<bool, Error> {
let url = format!("{}/collections/{}", self.url_base, collection_name);
let mut headers = BTreeMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
if let Some(api_key) = &self.api_key {
headers.insert("api-key".to_string(), api_key.clone());
}
let response = http::request::<()>(
&HttpRequest {
url: url.clone(),
headers,
method: Some("DELETE".to_string()),
},
None,
)?;
let json: Value = serde_json::from_slice(&response.body())?;
let success = json
.get("result")
.and_then(|v| v.as_bool())
.ok_or_else(|| anyhow!("[qdrant] Invalid response format"))?;
Ok(success)
}
pub fn upsert_points_api(&self, collection_name: &str, params: &Value) -> Result<(), Error> {
let url = format!(
"{}/collections/{}/points?wait=true",
self.url_base, collection_name,
);
let mut headers = BTreeMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
if let Some(api_key) = &self.api_key {
headers.insert("api-key".to_string(), api_key.clone());
}
let body = serde_json::to_vec(params)?;
let response = http::request(
&HttpRequest {
url: url.clone(),
headers,
method: Some("PUT".to_string()),
},
Some(&body),
)?;
let json: Value = serde_json::from_slice(&response.body())?;
let status = json
.get("status")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("[qdrant] Invalid response format"))?;
if status == "ok" {
Ok(())
} else {
Err(anyhow!(
"[qdrant] Failed to upsert points. Status = {}",
status
))
}
}
pub fn search_points_api(&self, collection_name: &str, params: &Value) -> Result<Value, Error> {
let url = format!(
"{}/collections/{}/points/search",
self.url_base, collection_name,
);
let mut headers = BTreeMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
if let Some(api_key) = &self.api_key {
headers.insert("api-key".to_string(), api_key.clone());
}
let body = serde_json::to_vec(params)?;
let response = http::request(
&HttpRequest {
url: url.clone(),
headers,
method: Some("POST".to_string()),
},
Some(&body),
)?;
let json: Value = serde_json::from_slice(&response.body())?;
Ok(json)
}
pub fn get_points_api(&self, collection_name: &str, params: &Value) -> Result<Value, Error> {
let url = format!("{}/collections/{}/points", self.url_base, collection_name);
let mut headers = BTreeMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
if let Some(api_key) = &self.api_key {
headers.insert("api-key".to_string(), api_key.clone());
}
let body = serde_json::to_vec(params)?;
let response = http::request(
&HttpRequest {
url: url.clone(),
headers,
method: Some("POST".to_string()),
},
Some(&body),
)?;
let json: Value = serde_json::from_slice(&response.body())?;
Ok(json)
}
pub fn get_point_api(&self, collection_name: &str, id: &PointId) -> Result<Value, Error> {
let url = format!(
"{}/collections/{}/points/{}",
self.url_base, collection_name, id,
);
let mut headers = BTreeMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
if let Some(api_key) = &self.api_key {
headers.insert("api-key".to_string(), api_key.clone());
}
let response = http::request::<()>(
&HttpRequest {
url: url.clone(),
headers,
method: Some("GET".to_string()),
},
None,
)?;
let json: Value = serde_json::from_slice(&response.body())?;
Ok(json)
}
pub fn delete_points_api(&self, collection_name: &str, params: &Value) -> Result<(), Error> {
let url = format!(
"{}/collections/{}/points/delete?wait=true",
self.url_base, collection_name,
);
let mut headers = BTreeMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
if let Some(api_key) = &self.api_key {
headers.insert("api-key".to_string(), api_key.clone());
}
let body = serde_json::to_vec(params)?;
let response = http::request(
&HttpRequest {
url: url.clone(),
headers,
method: Some("POST".to_string()),
},
Some(&body),
)?;
Ok(())
}
}
```
--------------------------------------------------------------------------------
/RUNTIME_CONFIG.md:
--------------------------------------------------------------------------------
```markdown
# Runtime Configuration
## Structure
The configuration is structured as follows:
- **auths** (`object`, optional): Authentication configurations for HTTPS requests, keyed by URL.
- **plugins**: A map of plugin names to plugin configuration objects.
- **path** (`string`): OCI path or HTTP URL or local path for the plugin.
- **runtime_config** (`object`, optional): Plugin-specific runtime configuration. The available fields are:
- **skip_tools** (`array[string]`, optional): List of regex patterns for tool names to skip loading at runtime. Each pattern is automatically anchored to match the entire tool name (equivalent to wrapping with `^` and `$`). Supports full regex syntax for powerful pattern matching.
- **allowed_hosts** (`array[string]`, optional): List of allowed hosts for the plugin (e.g., `["1.1.1.1"]` or `["*"]`).
- **allowed_paths** (`array[string]`, optional): List of allowed file system paths.
- **env_vars** (`object`, optional): Key-value pairs of environment variables for the plugin.
- **memory_limit** (`string`, optional): Memory limit for the plugin (e.g., `"512Mi"`).
## Plugin Names
Plugin names must follow strict naming conventions to ensure consistency and avoid conflicts:
### Allowed Characters
- **Letters**: A-Z, a-z (case-sensitive)
- **Numbers**: 0-9
- **Underscores**: _ (as separators only)
### Naming Rules
- Must start with a letter or number (not underscore)
- Must end with a letter or number (not underscore)
- Cannot contain consecutive underscores
- Cannot contain hyphens or other special characters
- Cannot contain spaces or whitespace
### Valid Examples
```
✅ plugin
✅ myPlugin
✅ plugin_name
✅ plugin123
✅ my_awesome_plugin_v2
✅ Plugin_Name_123
```
### Invalid Examples
```
❌ plugin-name (hyphens not allowed)
❌ plugin_ (cannot end with underscore)
❌ _plugin (cannot start with underscore)
❌ plugin__name (consecutive underscores)
❌ plugin name (spaces not allowed)
❌ plugin@name (special characters not allowed)
```
### Best Practices
- Use descriptive, meaningful names
- Follow consistent naming conventions within your organization
- Consider using prefixes for related plugins (e.g., `company_auth`, `company_logging`)
- Use underscores to separate logical components (e.g., `api_client`, `data_processor`)
## Authentication Configuration
The `auths` field allows you to configure authentication for HTTPS requests made by plugins. Authentication is matched by URL prefix, with longer prefixes taking precedence.
### Supported Authentication Types
#### Basic Authentication
```yaml
auths:
"https://api.example.com":
type: basic
username: "your-username"
password: "your-password"
```
#### Bearer Token Authentication
```yaml
auths:
"https://api.example.com":
type: token
token: "your-bearer-token"
```
#### Keyring Authentication
```yaml
auths:
"https://private.registry.io":
type: keyring
service: "my-app"
user: "registry-user"
```
### Keyring Setup Examples
For keyring authentication, you need to store the actual auth configuration JSON in your system keyring. This provides secure credential storage without exposing sensitive data in config files.
#### macOS (using Keychain Access or security command)
**Using the `security` command:**
```bash
# Store basic auth credentials
security add-generic-password -a "registry-user" -s "my-app" -w '{"type":"basic","username":"actual-user","password":"actual-pass"}'
# Store token auth credentials
security add-generic-password -a "api-user" -s "my-service" -w '{"type":"token","token":"actual-bearer-token"}'
# Verify the entry was created
security find-generic-password -a "registry-user" -s "my-app"
```
**Using Keychain Access GUI:**
1. Open Keychain Access (Applications → Utilities → Keychain Access)
2. Click "File" → "New Password Item"
3. Set "Keychain Item Name" to your service name (e.g., "my-app")
4. Set "Account Name" to your user name (e.g., "registry-user")
5. Set "Password" to the JSON auth config: `{"type":"basic","username":"actual-user","password":"actual-pass"}`
6. Click "Add"
#### Linux (using libsecret/gnome-keyring)
**Install required tools:**
```bash
# Ubuntu/Debian
sudo apt-get install libsecret-tools
# RHEL/CentOS/Fedora
sudo yum install libsecret-devel
```
**Using `secret-tool`:**
```bash
# Store basic auth credentials
echo '{"type":"basic","username":"actual-user","password":"actual-pass"}' | secret-tool store --label="my-app credentials" service "my-app" username "registry-user"
# Store token auth credentials
echo '{"type":"token","token":"actual-bearer-token"}' | secret-tool store --label="my-service token" service "my-service" username "api-user"
# Verify the entry was created
secret-tool lookup service "my-app" username "registry-user"
```
#### Windows (using Windows Credential Manager)
**Using `cmdkey` (Command Prompt as Administrator):**
```cmd
REM Store basic auth credentials (escape quotes for JSON)
cmdkey /generic:"my-app" /user:"registry-user" /pass:"{\"type\":\"basic\",\"username\":\"actual-user\",\"password\":\"actual-pass\"}"
REM Store token auth credentials
cmdkey /generic:"my-service" /user:"api-user" /pass:"{\"type\":\"token\",\"token\":\"actual-bearer-token\"}"
REM Verify the entry was created
cmdkey /list:"my-app"
```
**Using Credential Manager GUI:**
1. Open "Credential Manager" from Control Panel → User Accounts → Credential Manager
2. Click "Add a generic credential"
3. Set "Internet or network address" to your service name (e.g., "my-app")
4. Set "User name" to your user name (e.g., "registry-user")
5. Set "Password" to the JSON auth config: `{"type":"basic","username":"actual-user","password":"actual-pass"}`
6. Click "OK"
**Using PowerShell:**
```powershell
# Store basic auth credentials
$cred = New-Object System.Management.Automation.PSCredential("registry-user", (ConvertTo-SecureString '{"type":"basic","username":"actual-user","password":"actual-pass"}' -AsPlainText -Force))
New-StoredCredential -Target "my-app" -Credential $cred -Type Generic
```
### URL Matching Behavior
Authentication is applied based on URL prefix matching:
- Longer prefixes take precedence over shorter ones
- Exact matches take highest precedence
- URLs are matched case-sensitively
**Example:**
```yaml
auths:
"https://example.com":
type: basic
username: "broad-user"
password: "broad-pass"
"https://example.com/api":
type: token
token: "api-token"
"https://example.com/api/v1":
type: basic
username: "v1-user"
password: "v1-pass"
```
- Request to `https://example.com/api/v1/users` → uses v1 basic auth (longest match)
- Request to `https://example.com/api/data` → uses api token auth
- Request to `https://example.com/public` → uses broad basic auth
### Keyring Authentication Example
**Configuration file:**
```yaml
auths:
"https://private.registry.io":
type: keyring
service: "private-registry"
user: "registry-user"
"https://internal.company.com":
type: keyring
service: "company-api"
user: "api-user"
plugins:
secure-plugin:
url: "https://private.registry.io/secure-plugin"
runtime_config:
allowed_hosts:
- "private.registry.io"
```
**Corresponding keyring entries (stored separately):**
- Service: `private-registry`, User: `registry-user`, Password: `{"type":"basic","username":"real-user","password":"real-pass"}`
- Service: `company-api`, User: `api-user`, Password: `{"type":"token","token":"company-jwt-token"}`
### Real-World Keyring Scenarios
#### Scenario 1: Corporate Environment
```yaml
auths:
"https://artifactory.company.com":
type: keyring
service: "company-artifactory"
user: "build-service"
"https://nexus.company.com":
type: keyring
service: "company-nexus"
user: "deployment-bot"
```
Setup corporate credentials once:
```bash
# macOS
security add-generic-password -a "build-service" -s "company-artifactory" -w '{"type":"basic","username":"corp_user","password":"corp_secret"}'
# Linux
echo '{"type":"basic","username":"corp_user","password":"corp_secret"}' | secret-tool store --label="Company Artifactory" service "company-artifactory" username "build-service"
# Windows
cmdkey /generic:"company-artifactory" /user:"build-service" /pass:"{\"type\":\"basic\",\"username\":\"corp_user\",\"password\":\"corp_secret\"}"
```
#### Scenario 2: Multi-Environment Setup
```yaml
auths:
"https://staging-api.example.com":
type: keyring
service: "example-staging"
user: "staging-user"
"https://prod-api.example.com":
type: keyring
service: "example-prod"
user: "prod-user"
```
Store different credentials for each environment:
```bash
# Staging credentials
security add-generic-password -a "staging-user" -s "example-staging" -w '{"type":"token","token":"staging-jwt-token"}'
# Production credentials
security add-generic-password -a "prod-user" -s "example-prod" -w '{"type":"token","token":"prod-jwt-token"}'
```
#### Scenario 3: Team Shared Configuration
```yaml
# Team members can share this config file safely
auths:
"https://shared-registry.team.com":
type: keyring
service: "team-registry"
user: "developer"
```
Each team member stores their own credentials:
```bash
# Developer A
security add-generic-password -a "developer" -s "team-registry" -w '{"type":"basic","username":"alice","password":"alice_key"}'
# Developer B
security add-generic-password -a "developer" -s "team-registry" -w '{"type":"basic","username":"bob","password":"bob_key"}'
```
### Keyring Best Practices
1. **Service Naming Convention**: Use descriptive, consistent service names (e.g., `company-artifactory`, `project-registry`)
2. **User Identification**: Use role-based usernames (e.g., `build-service`, `deployment-bot`) rather than personal names
3. **Credential Rotation**: Update keyring entries when rotating credentials - no config file changes needed
4. **Environment Separation**: Use different service names for different environments
5. **Team Coordination**: Document your service/user naming conventions for team members
6. **Backup Strategy**: Consider backing up keyring entries for critical services
7. **Testing**: Use non-production credentials in keyring for testing
## Example (YAML)
```yaml
auths:
"https://private.registry.io":
type: basic
username: "registry-user"
password: "registry-pass"
"https://api.github.com":
type: token
token: "ghp_1234567890abcdef"
"https://enterprise.api.com":
type: basic
username: "enterprise-user"
password: "enterprise-pass"
plugins:
time:
url: oci://ghcr.io/tuananh/time-plugin:latest
myip:
url: oci://ghcr.io/tuananh/myip-plugin:latest
runtime_config:
allowed_hosts:
- "1.1.1.1"
skip_tools:
- "debug_tool" # Skip exact tool name
- "temp_.*" # Skip tools starting with "temp_"
- ".*_backup" # Skip tools ending with "_backup"
- "test_[0-9]+" # Skip tools like "test_1", "test_42"
env_vars:
FOO: "bar"
memory_limit: "512Mi"
private_plugin:
url: "https://private.registry.io/my-plugin"
runtime_config:
allowed_hosts:
- "private.registry.io"
```
## Example (JSON)
```json
{
"auths": {
"https://private.registry.io": {
"type": "basic",
"username": "registry-user",
"password": "registry-pass"
},
"https://api.github.com": {
"type": "token",
"token": "ghp_1234567890abcdef"
},
"https://enterprise.api.com": {
"type": "basic",
"username": "enterprise-user",
"password": "enterprise-pass"
}
},
"plugins": {
"time": {
"url": "oci://ghcr.io/tuananh/time-plugin:latest"
},
"myip": {
"url": "oci://ghcr.io/tuananh/myip-plugin:latest",
"runtime_config": {
"allowed_hosts": ["1.1.1.1"],
"skip_tools": [
"debug_tool",
"temp_.*",
".*_backup",
"test_[0-9]+"
],
"env_vars": {"FOO": "bar"},
"memory_limit": "512Mi"
}
},
"private_plugin": {
"url": "https://private.registry.io/my-plugin",
"runtime_config": {
"allowed_hosts": ["private.registry.io"]
}
}
}
}
```
## Loading Configuration
Configuration is loaded at runtime from a file with `.json`, `.yaml`, `.yml`, or `.toml` extension. The loader will parse the file according to its extension. If the file does not exist or the format is unsupported, an error will be raised.
## Security Considerations
### Credential Storage
- **Basic/Token auth**: Credentials are stored directly in the config file. Ensure proper file permissions (e.g., `chmod 600`).
- **Keyring auth**: Credentials are stored securely in the system keyring. The config file only contains service/user identifiers.
### Best Practices
- Use keyring authentication for production environments
- Rotate credentials regularly
- Use environment-specific config files
- Never commit credentials to version control
- Consider using short-lived tokens when possible
## Troubleshooting Keyring Authentication
### Common Issues
#### "No matching entry found in secure storage"
This error occurs when the keyring entry doesn't exist or can't be accessed.
**Solutions:**
1. Verify the service and user names match exactly between config and keyring
2. Check that the keyring entry exists:
```bash
# macOS
security find-generic-password -a "your-user" -s "your-service"
# Linux
secret-tool lookup service "your-service" username "your-user"
# Windows
cmdkey /list:"your-service"
```
3. Ensure the current user has permission to access the keyring entry
#### "Failed to parse JSON from keyring"
This error occurs when the stored password isn't valid JSON or doesn't match the expected AuthConfig format.
**Solutions:**
1. Verify the stored password is valid JSON:
```bash
# macOS - retrieve and validate
security find-generic-password -a "your-user" -s "your-service" -w | jq .
```
2. Ensure the JSON matches one of these formats:
- `{"type":"basic","username":"real-user","password":"real-pass"}`
- `{"type":"token","token":"real-token"}`
#### Platform-Specific Issues
**macOS:**
- Keychain may be locked - unlock it manually or use `security unlock-keychain`
- Application may not have keychain access permissions
**Linux:**
- GNOME Keyring service may not be running: `systemctl --user status gnome-keyring`
- D-Bus session may not be available in non-graphical environments
**Windows:**
- Credential Manager may require administrator privileges for certain operations
- Windows Credential Manager has size limits for stored passwords
### Debugging Tips
1. **Test keyring access independently:**
```bash
# Create a test entry
security add-generic-password -a "test-user" -s "test-service" -w '{"type":"token","token":"test"}'
# Retrieve it
security find-generic-password -a "test-user" -s "test-service" -w
# Clean up
security delete-generic-password -a "test-user" -s "test-service"
```
2. **Validate JSON format:**
```bash
echo '{"type":"basic","username":"user","password":"pass"}' | jq .
```
3. **Check permissions:**
```bash
# Ensure config file is readable
ls -la config.yaml
# Set appropriate permissions
chmod 600 config.yaml
```
## Skip Tools Pattern Matching
The `skip_tools` field supports powerful regex pattern matching for filtering out unwanted tools at runtime.
> 📖 **For comprehensive examples, advanced patterns, and detailed use cases, see [SKIP_TOOLS_GUIDE.md](./SKIP_TOOLS_GUIDE.md)**
### Pattern Behavior
- **Automatic Anchoring**: Patterns are automatically anchored to match the entire tool name (wrapped with `^` and `$`)
- **Regex Support**: Full regex syntax is supported, including wildcards, character classes, and quantifiers
- **Case Sensitive**: Pattern matching is case-sensitive
- **Compilation**: All patterns are compiled into a single optimized regex set for efficient matching
### Pattern Examples
#### Exact Matches
```yaml
skip_tools:
- "debug_tool" # Matches only "debug_tool"
- "test_runner" # Matches only "test_runner"
```
#### Wildcard Patterns
```yaml
skip_tools:
- "temp_.*" # Matches "temp_file", "temp_data", etc.
- ".*_backup" # Matches "data_backup", "file_backup", etc.
- "debug.*" # Matches "debug", "debugger", "debug_info", etc.
```
#### Advanced Regex Patterns
```yaml
skip_tools:
- "tool_[0-9]+" # Matches "tool_1", "tool_42", etc.
- "test_(unit|integration)" # Matches "test_unit" and "test_integration"
- "[a-z]+_helper" # Matches lowercase word + "_helper"
- "system_(admin|user)_.*" # Matches tools starting with "system_admin_" or "system_user_"
```
#### Explicit Anchoring
```yaml
skip_tools:
- "^prefix_.*" # Explicit start anchor (same as "prefix_.*" due to auto-anchoring)
- ".*_suffix$" # Explicit end anchor (same as ".*_suffix" due to auto-anchoring)
- "^exact_only$" # Fully explicit anchoring (same as "exact_only")
```
#### Special Characters
```yaml
skip_tools:
- "file\\.exe" # Matches "file.exe" literally (escaped dot)
- "script\\?" # Matches "script?" literally (escaped question mark)
- "temp\\*data" # Matches "temp*data" literally (escaped asterisk)
```
#### Common Use Cases
```yaml
skip_tools:
- ".*_test" # Skip all test tools
- "dev_.*" # Skip all development tools
- "mock_.*" # Skip all mock tools
- ".*_deprecated" # Skip all deprecated tools
- "admin_.*" # Skip all admin tools
- "debug.*" # Skip all debug-related tools
```
### Error Handling
- Invalid regex patterns will cause configuration loading to fail with a descriptive error
- Empty pattern arrays are allowed and will skip no tools
- The `skip_tools` field can be omitted entirely to skip no tools
### Performance Notes
- All patterns are compiled into a single optimized `RegexSet` for O(1) tool name checking
- Pattern compilation happens once at startup, not per tool evaluation
- Large numbers of patterns have minimal runtime performance impact
## Notes
- Fields marked as `optional` can be omitted.
- Plugin authors may extend `runtime_config` with additional fields, but only the above are officially recognized.
- Authentication applies to all HTTPS requests made by plugins, including plugin downloads and runtime API calls.
- URL matching is case-sensitive and based on string prefix matching.
- Keyring authentication requires platform-specific keyring services to be available and accessible.
- Skip tools patterns use full regex syntax with automatic anchoring for precise tool filtering.
```
--------------------------------------------------------------------------------
/src/naming.rs:
--------------------------------------------------------------------------------
```rust
use crate::config::{PluginName, PluginNameParseError};
use anyhow::Result;
use std::fmt;
use std::str::FromStr;
use url::Url;
#[derive(Debug, Clone)]
pub struct NamespacedNameParseError;
impl fmt::Display for NamespacedNameParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Failed to parse name")
}
}
impl std::error::Error for NamespacedNameParseError {}
impl From<PluginNameParseError> for NamespacedNameParseError {
fn from(_: PluginNameParseError) -> Self {
NamespacedNameParseError
}
}
pub fn create_namespaced_name(plugin_name: &PluginName, name: &str) -> String {
format!("{plugin_name}-{name}")
}
pub fn create_namespaced_uri(plugin_name: &PluginName, uri: &str) -> Result<String> {
let mut uri = Url::parse(uri)?;
uri.set_path(&format!(
"{}/{}",
plugin_name.as_str(),
uri.path().trim_start_matches('/')
));
Ok(uri.to_string())
}
pub fn parse_namespaced_name(namespaced_name: String) -> Result<(PluginName, String)> {
if let Some((plugin_name, tool_name)) = namespaced_name.split_once("-") {
return Ok((PluginName::from_str(plugin_name)?, tool_name.to_string()));
}
Err(NamespacedNameParseError.into())
}
pub fn parse_namespaced_uri(namespaced_uri: String) -> Result<(PluginName, String)> {
let mut uri = Url::parse(namespaced_uri.as_str())?;
let mut segments = uri
.path_segments()
.ok_or(url::ParseError::RelativeUrlWithoutBase)?
.collect::<Vec<&str>>();
if segments.is_empty() {
return Err(NamespacedNameParseError.into());
}
let plugin_name = PluginName::from_str(segments.remove(0))?;
uri.set_path(&segments.join("/"));
Ok((plugin_name, uri.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_tool_name() {
let plugin_name = PluginName::from_str("example_plugin").unwrap();
let tool_name = "example_tool";
let expected = "example_plugin-example_tool";
assert_eq!(create_namespaced_name(&plugin_name, tool_name), expected);
}
#[test]
fn test_parse_tool_name() {
let tool_name = "example_plugin-example_tool".to_string();
let result = parse_namespaced_name(tool_name);
assert!(result.is_ok());
let (plugin_name, tool) = result.unwrap();
assert_eq!(plugin_name.as_str(), "example_plugin");
assert_eq!(tool, "example_tool");
}
#[test]
fn test_create_tool_name_invalid() {
let plugin_name = PluginName::from_str("example_plugin").unwrap();
let tool_name = "invalid-tool";
let result = create_namespaced_name(&plugin_name, tool_name);
assert_eq!(result, "example_plugin-invalid-tool");
}
#[test]
fn test_create_namespaced_tool_name_with_special_chars() {
let plugin_name = PluginName::from_str("test_plugin_123").unwrap();
let tool_name = "tool_name_with_underscores";
let result = create_namespaced_name(&plugin_name, tool_name);
assert_eq!(result, "test_plugin_123-tool_name_with_underscores");
}
#[test]
fn test_create_namespaced_tool_name_empty_tool_name() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let tool_name = "";
let result = create_namespaced_name(&plugin_name, tool_name);
assert_eq!(result, "test_plugin-");
}
#[test]
fn test_create_namespaced_tool_name_multiple_hyphens() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let tool_name = "invalid-tool-name";
let result = create_namespaced_name(&plugin_name, tool_name);
assert_eq!(result, "test_plugin-invalid-tool-name");
}
#[test]
fn test_parse_namespaced_tool_name_with_special_chars() {
let tool_name = "plugin_name_123-tool_name_456".to_string();
let result = parse_namespaced_name(tool_name).unwrap();
assert_eq!(result.0.as_str(), "plugin_name_123");
assert_eq!(result.1, "tool_name_456");
}
#[test]
fn test_parse_namespaced_tool_name_no_separator() {
let tool_name = "invalid_tool_name".to_string();
let result = parse_namespaced_name(tool_name);
assert!(result.is_err());
}
#[test]
fn test_parse_namespaced_tool_name_multiple_separators() {
let tool_name = "plugin-tool-extra".to_string();
let result = parse_namespaced_name(tool_name).unwrap();
assert_eq!(result.0.as_str(), "plugin");
assert_eq!(result.1, "tool-extra");
}
#[test]
fn test_parse_namespaced_tool_name_empty_parts() {
let tool_name = "-tool".to_string();
let result = parse_namespaced_name(tool_name);
// This should still work but with empty plugin name
if result.is_ok() {
let (plugin, _) = result.unwrap();
assert!(plugin.as_str().is_empty());
}
}
#[test]
fn test_parse_namespaced_tool_name_only_separator() {
let tool_name = "-".to_string();
let result = parse_namespaced_name(tool_name);
// Should result in empty plugin and tool names
if let Ok((plugin, tool)) = result {
assert!(plugin.as_str().is_empty());
assert!(tool.is_empty());
}
}
#[test]
fn test_parse_namespaced_tool_name_empty_string() {
let tool_name = "".to_string();
let result = parse_namespaced_name(tool_name);
assert!(result.is_err());
}
#[test]
fn test_tool_name_parse_error_display() {
let error = NamespacedNameParseError;
assert_eq!(format!("{error}"), "Failed to parse name");
}
#[test]
fn test_tool_name_parse_error_from_plugin_name_error() {
let plugin_error = PluginNameParseError;
let tool_error: NamespacedNameParseError = plugin_error.into();
assert_eq!(format!("{tool_error}"), "Failed to parse name");
}
#[test]
fn test_round_trip_tool_name_operations() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let original_tool = "my_tool";
let namespaced = create_namespaced_name(&plugin_name, original_tool);
let (parsed_plugin, parsed_tool) = parse_namespaced_name(namespaced).unwrap();
assert_eq!(parsed_plugin.as_str(), "test_plugin");
assert_eq!(parsed_tool, "my_tool");
}
#[test]
fn test_tool_name_with_unicode() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let tool_name = "тест_工具"; // Cyrillic and Chinese characters
let result = create_namespaced_name(&plugin_name, tool_name);
assert_eq!(result, "test_plugin-тест_工具");
}
#[test]
fn test_very_long_tool_names() {
let plugin_name = PluginName::from_str("plugin").unwrap();
let very_long_tool = "a".repeat(1000);
let namespaced = create_namespaced_name(&plugin_name, &very_long_tool);
let (parsed_plugin, parsed_tool) = parse_namespaced_name(namespaced).unwrap();
assert_eq!(parsed_plugin.as_str(), "plugin");
assert_eq!(parsed_tool.len(), 1000);
}
#[test]
fn test_plugin_name_error_conversion() {
let plugin_error = PluginNameParseError;
let tool_error: NamespacedNameParseError = plugin_error.into();
// Test that the error implements standard error traits
assert!(std::error::Error::source(&tool_error).is_none());
assert!(!format!("{tool_error}").is_empty());
}
#[test]
fn test_tool_name_with_numbers_and_special_chars() {
let plugin_name = PluginName::from_str("plugin_123").unwrap();
let tool_name = "tool_456_test";
let result = create_namespaced_name(&plugin_name, tool_name);
assert_eq!(result, "plugin_123-tool_456_test");
let (parsed_plugin, parsed_tool) = parse_namespaced_name(result).unwrap();
assert_eq!(parsed_plugin.as_str(), "plugin_123");
assert_eq!(parsed_tool, "tool_456_test");
}
#[test]
fn test_borrowed_vs_owned_cow_strings() {
// Test with borrowed string
let borrowed_result = parse_namespaced_name("plugin-tool".to_string());
assert!(borrowed_result.is_ok());
// Test with owned string
let owned_result = parse_namespaced_name("plugin-tool".to_string());
assert!(owned_result.is_ok());
let (plugin1, tool1) = borrowed_result.unwrap();
let (plugin2, tool2) = owned_result.unwrap();
assert_eq!(plugin1.as_str(), plugin2.as_str());
assert_eq!(tool1, tool2);
}
#[test]
fn test_namespaced_tool_format_invariants() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let tool_name = "test_tool";
let namespaced = create_namespaced_name(&plugin_name, tool_name);
// Should contain at least one "-" (the separator)
let hyphen_count = namespaced.matches("-").count();
assert!(hyphen_count >= 1, "Should contain at least one '-'");
// Should start with plugin name
assert!(
namespaced.starts_with("test_plugin"),
"Should start with plugin name"
);
// Should end with tool name
assert!(
namespaced.ends_with("test_tool"),
"Should end with tool name"
);
// Should be in the format "plugin-tool"
assert_eq!(namespaced, "test_plugin-test_tool");
// Test parsing works correctly with the first hyphen as separator
let (parsed_plugin, parsed_tool) = parse_namespaced_name(namespaced).unwrap();
assert_eq!(parsed_plugin.as_str(), "test_plugin");
assert_eq!(parsed_tool, "test_tool");
}
// Tests for create_namespaced_uri and parse_namespaced_uri
#[test]
fn test_create_namespaced_uri_basic() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let uri = "http://example.com/api/endpoint";
let result = create_namespaced_uri(&plugin_name, uri).unwrap();
assert_eq!(result, "http://example.com/test_plugin/api/endpoint");
}
#[test]
fn test_create_namespaced_uri_root_path() {
let plugin_name = PluginName::from_str("my_plugin").unwrap();
let uri = "http://example.com/";
let result = create_namespaced_uri(&plugin_name, uri).unwrap();
assert_eq!(result, "http://example.com/my_plugin/");
}
#[test]
fn test_create_namespaced_uri_no_path() {
let plugin_name = PluginName::from_str("my_plugin").unwrap();
let uri = "http://example.com";
let result = create_namespaced_uri(&plugin_name, uri).unwrap();
assert_eq!(result, "http://example.com/my_plugin/");
}
#[test]
fn test_create_namespaced_uri_with_query_string() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let uri = "http://example.com/api/endpoint?key=value&foo=bar";
let result = create_namespaced_uri(&plugin_name, uri).unwrap();
// Query string should be preserved
assert!(result.contains("test_plugin/api/endpoint"));
assert!(result.contains("key=value"));
assert!(result.contains("foo=bar"));
}
#[test]
fn test_create_namespaced_uri_with_fragment() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let uri = "http://example.com/api/endpoint#section";
let result = create_namespaced_uri(&plugin_name, uri).unwrap();
assert!(result.contains("test_plugin/api/endpoint"));
assert!(result.contains("#section"));
}
#[test]
fn test_create_namespaced_uri_with_port() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let uri = "http://example.com:8080/api/endpoint";
let result = create_namespaced_uri(&plugin_name, uri).unwrap();
assert_eq!(result, "http://example.com:8080/test_plugin/api/endpoint");
}
#[test]
fn test_create_namespaced_uri_https() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let uri = "https://secure.example.com/api/endpoint";
let result = create_namespaced_uri(&plugin_name, uri).unwrap();
assert_eq!(
result,
"https://secure.example.com/test_plugin/api/endpoint"
);
}
#[test]
fn test_create_namespaced_uri_leading_slash_path() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let uri = "http://example.com//api/endpoint";
let result = create_namespaced_uri(&plugin_name, uri).unwrap();
assert!(result.contains("test_plugin"));
}
#[test]
fn test_create_namespaced_uri_deep_path() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let uri = "http://example.com/v1/api/v2/endpoint/deep";
let result = create_namespaced_uri(&plugin_name, uri).unwrap();
assert_eq!(
result,
"http://example.com/test_plugin/v1/api/v2/endpoint/deep"
);
}
#[test]
fn test_create_namespaced_uri_invalid_url() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let uri = "not a valid url";
let result = create_namespaced_uri(&plugin_name, uri);
assert!(result.is_err());
}
#[test]
fn test_create_namespaced_uri_with_underscores_in_plugin_name() {
let plugin_name = PluginName::from_str("my_test_plugin_123").unwrap();
let uri = "http://example.com/api";
let result = create_namespaced_uri(&plugin_name, uri).unwrap();
assert_eq!(result, "http://example.com/my_test_plugin_123/api");
}
#[test]
fn test_parse_namespaced_uri_basic() {
let namespaced_uri = "http://example.com/test_plugin/api/endpoint".to_string();
let (plugin_name, uri) = parse_namespaced_uri(namespaced_uri).unwrap();
assert_eq!(plugin_name.as_str(), "test_plugin");
assert_eq!(uri, "http://example.com/api/endpoint");
}
#[test]
fn test_parse_namespaced_uri_root_path() {
let namespaced_uri = "http://example.com/my_plugin/".to_string();
let (plugin_name, uri) = parse_namespaced_uri(namespaced_uri).unwrap();
assert_eq!(plugin_name.as_str(), "my_plugin");
assert_eq!(uri, "http://example.com/");
}
#[test]
fn test_parse_namespaced_uri_with_query_string() {
let namespaced_uri = "http://example.com/test_plugin/api/endpoint?key=value".to_string();
let (plugin_name, uri) = parse_namespaced_uri(namespaced_uri).unwrap();
assert_eq!(plugin_name.as_str(), "test_plugin");
assert!(uri.contains("api/endpoint"));
assert!(uri.contains("key=value"));
}
#[test]
fn test_parse_namespaced_uri_with_fragment() {
let namespaced_uri = "http://example.com/test_plugin/api/endpoint#section".to_string();
let (plugin_name, uri) = parse_namespaced_uri(namespaced_uri).unwrap();
assert_eq!(plugin_name.as_str(), "test_plugin");
assert!(uri.contains("api/endpoint"));
assert!(uri.contains("#section"));
}
#[test]
fn test_parse_namespaced_uri_with_port() {
let namespaced_uri = "http://example.com:8080/test_plugin/api/endpoint".to_string();
let (plugin_name, uri) = parse_namespaced_uri(namespaced_uri).unwrap();
assert_eq!(plugin_name.as_str(), "test_plugin");
assert_eq!(uri, "http://example.com:8080/api/endpoint");
}
#[test]
fn test_parse_namespaced_uri_https() {
let namespaced_uri = "https://secure.example.com/test_plugin/api/endpoint".to_string();
let (plugin_name, uri) = parse_namespaced_uri(namespaced_uri).unwrap();
assert_eq!(plugin_name.as_str(), "test_plugin");
assert_eq!(uri, "https://secure.example.com/api/endpoint");
}
#[test]
fn test_parse_namespaced_uri_deep_path() {
let namespaced_uri = "http://example.com/test_plugin/v1/api/v2/endpoint/deep".to_string();
let (plugin_name, uri) = parse_namespaced_uri(namespaced_uri).unwrap();
assert_eq!(plugin_name.as_str(), "test_plugin");
assert_eq!(uri, "http://example.com/v1/api/v2/endpoint/deep");
}
#[test]
fn test_parse_namespaced_uri_invalid_url() {
let namespaced_uri = "not a valid url".to_string();
let result = parse_namespaced_uri(namespaced_uri);
assert!(result.is_err());
}
#[test]
fn test_parse_namespaced_uri_no_path() {
let namespaced_uri = "http://example.com".to_string();
let result = parse_namespaced_uri(namespaced_uri);
// Should fail because there's no path segment for plugin name
assert!(result.is_err());
}
#[test]
fn test_parse_namespaced_uri_only_plugin() {
let namespaced_uri = "http://example.com/test_plugin".to_string();
let (plugin_name, uri) = parse_namespaced_uri(namespaced_uri).unwrap();
assert_eq!(plugin_name.as_str(), "test_plugin");
assert_eq!(uri, "http://example.com/");
}
#[test]
fn test_round_trip_uri_operations() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let original_uri = "http://example.com/api/endpoint";
let namespaced = create_namespaced_uri(&plugin_name, original_uri).unwrap();
let (parsed_plugin, parsed_uri) = parse_namespaced_uri(namespaced).unwrap();
assert_eq!(parsed_plugin.as_str(), "test_plugin");
assert_eq!(parsed_uri, original_uri);
}
#[test]
fn test_round_trip_uri_with_query_and_fragment() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let original_uri = "http://example.com/api/endpoint?key=value#section";
let namespaced = create_namespaced_uri(&plugin_name, original_uri).unwrap();
let (parsed_plugin, parsed_uri) = parse_namespaced_uri(namespaced).unwrap();
assert_eq!(parsed_plugin.as_str(), "test_plugin");
assert_eq!(parsed_uri, original_uri);
}
#[test]
fn test_uri_with_special_characters_in_path() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let uri = "http://example.com/api/resource-123_test";
let namespaced = create_namespaced_uri(&plugin_name, uri).unwrap();
assert_eq!(
namespaced,
"http://example.com/test_plugin/api/resource-123_test"
);
let (parsed_plugin, parsed_uri) = parse_namespaced_uri(namespaced).unwrap();
assert_eq!(parsed_plugin.as_str(), "test_plugin");
assert_eq!(parsed_uri, uri);
}
#[test]
fn test_create_namespaced_uri_with_empty_path() {
let plugin_name = PluginName::from_str("test_plugin").unwrap();
let uri = "http://example.com/";
let result = create_namespaced_uri(&plugin_name, uri).unwrap();
assert_eq!(result, "http://example.com/test_plugin/");
}
#[test]
fn test_parse_namespaced_uri_with_underscores_in_plugin() {
let namespaced_uri = "http://example.com/my_test_plugin_123/api/resource".to_string();
let (plugin_name, uri) = parse_namespaced_uri(namespaced_uri).unwrap();
assert_eq!(plugin_name.as_str(), "my_test_plugin_123");
assert_eq!(uri, "http://example.com/api/resource");
}
}
```
--------------------------------------------------------------------------------
/examples/plugins/v1/fs/src/lib.rs:
--------------------------------------------------------------------------------
```rust
mod pdk;
use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::Path;
use std::time::SystemTime;
use extism_pdk::*;
use json::Value;
use pdk::types::{
CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
};
use serde_json::json;
pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
info!("call: {:?}", input);
match input.params.name.as_str() {
"read_file" => read_file(input),
"read_multiple_files" => read_multiple_files(input),
"write_file" => write_file(input),
"edit_file" => edit_file(input),
"create_dir" => create_dir(input),
"list_dir" => list_dir(input),
"move_file" => move_file(input),
"search_files" => search_files(input),
"get_file_info" => get_file_info(input),
_ => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Unknown operation: {}", input.params.name)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
}
fn read_file(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.clone().unwrap_or_default();
if let Some(Value::String(path)) = args.get("path") {
match fs::read_to_string(path) {
Ok(content) => Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(content),
mime_type: Some("text/plain".to_string()),
r#type: ContentType::Text,
data: None,
}],
}),
Err(e) => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Failed to read file: {}", e)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide a path".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
fn read_multiple_files(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.clone().unwrap_or_default();
if let Some(Value::Array(paths)) = args.get("paths") {
let mut results = Vec::new();
for path in paths {
if let Value::String(path_str) = path {
match fs::read_to_string(path_str) {
Ok(content) => results.push(json!({
"path": path_str,
"content": content,
"error": null
})),
Err(e) => results.push(json!({
"path": path_str,
"content": null,
"error": e.to_string()
})),
}
}
}
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(serde_json::to_string(&results)?),
mime_type: Some("application/json".to_string()),
r#type: ContentType::Text,
data: None,
}],
})
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide an array of paths".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
fn write_file(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.clone().unwrap_or_default();
if let (Some(Value::String(path)), Some(Value::String(content))) =
(args.get("path"), args.get("content"))
{
match fs::write(path, content) {
Ok(_) => Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some("File written successfully".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
Err(e) => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Failed to write file: {}", e)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide path and content".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
fn edit_file(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.clone().unwrap_or_default();
if let (Some(Value::String(path)), Some(Value::String(content))) =
(args.get("path"), args.get("content"))
{
let mut file = OpenOptions::new().write(true).truncate(true).open(path)?;
file.write_all(content.as_bytes())?;
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some("File edited successfully".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide path and content".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
fn create_dir(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.clone().unwrap_or_default();
if let Some(Value::String(path)) = args.get("path") {
match fs::create_dir_all(path) {
Ok(_) => Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some("Directory created successfully".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
Err(e) => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Failed to create directory: {}", e)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide a path".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
fn list_dir(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.clone().unwrap_or_default();
if let Some(Value::String(path)) = args.get("path") {
match fs::read_dir(path) {
Ok(entries) => {
let mut items = Vec::new();
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
let metadata = entry.metadata()?;
items.push(json!({
"name": entry.file_name().to_string_lossy(),
"path": path.to_string_lossy(),
"is_file": metadata.is_file(),
"is_dir": metadata.is_dir(),
"size": metadata.len(),
"modified": metadata.modified()?.duration_since(SystemTime::UNIX_EPOCH)?.as_secs()
}));
}
}
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(serde_json::to_string(&items)?),
mime_type: Some("application/json".to_string()),
r#type: ContentType::Text,
data: None,
}],
})
}
Err(e) => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Failed to list directory: {}", e)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide a path".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
fn move_file(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.clone().unwrap_or_default();
if let (Some(Value::String(from)), Some(Value::String(to))) = (args.get("from"), args.get("to"))
{
match fs::rename(from, to) {
Ok(_) => Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some("File moved successfully".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
Err(e) => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Failed to move file: {}", e)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide from and to paths".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
fn search_files(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.clone().unwrap_or_default();
if let (Some(Value::String(dir)), Some(Value::String(pattern))) =
(args.get("directory"), args.get("pattern"))
{
let mut results = Vec::new();
fn search_dir(dir: &Path, pattern: &str, results: &mut Vec<String>) -> io::Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
search_dir(&path, pattern, results)?;
} else if path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.contains(pattern)
{
results.push(path.to_string_lossy().into_owned());
}
}
Ok(())
}
match search_dir(Path::new(dir), pattern, &mut results) {
Ok(_) => Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(serde_json::to_string(&results)?),
mime_type: Some("application/json".to_string()),
r#type: ContentType::Text,
data: None,
}],
}),
Err(e) => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Failed to search files: {}", e)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide directory and pattern".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
fn get_file_info(input: CallToolRequest) -> Result<CallToolResult, Error> {
let args = input.params.arguments.clone().unwrap_or_default();
if let Some(Value::String(path)) = args.get("path") {
match fs::metadata(path) {
Ok(metadata) => {
let info = json!({
"size": metadata.len(),
"is_file": metadata.is_file(),
"is_dir": metadata.is_dir(),
"modified": metadata.modified()?.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
"created": metadata.created()?.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
"accessed": metadata.accessed()?.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
});
Ok(CallToolResult {
is_error: None,
content: vec![Content {
annotations: None,
text: Some(serde_json::to_string(&info)?),
mime_type: Some("application/json".to_string()),
r#type: ContentType::Text,
data: None,
}],
})
}
Err(e) => Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some(format!("Failed to get file info: {}", e)),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
}),
}
} else {
Ok(CallToolResult {
is_error: Some(true),
content: vec![Content {
annotations: None,
text: Some("Please provide a path".into()),
mime_type: None,
r#type: ContentType::Text,
data: None,
}],
})
}
}
pub(crate) fn describe() -> Result<ListToolsResult, Error> {
Ok(ListToolsResult {
tools: vec![
ToolDescription {
name: "read_file".into(),
description: "Read the contents of a file".into(),
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to read",
},
},
"required": ["path"],
})
.as_object()
.unwrap()
.clone(),
},
ToolDescription {
name: "read_multiple_files".into(),
description: "Read contents of multiple files".into(),
input_schema: json!({
"type": "object",
"properties": {
"paths": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of file paths to read",
},
},
"required": ["paths"],
})
.as_object()
.unwrap()
.clone(),
},
ToolDescription {
name: "write_file".into(),
description: "Write content to a file".into(),
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path where to write the file",
},
"content": {
"type": "string",
"description": "Content to write to the file",
},
},
"required": ["path", "content"],
})
.as_object()
.unwrap()
.clone(),
},
ToolDescription {
name: "edit_file".into(),
description: "Edit an existing file's content".into(),
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to edit",
},
"content": {
"type": "string",
"description": "New content for the file",
},
},
"required": ["path", "content"],
})
.as_object()
.unwrap()
.clone(),
},
ToolDescription {
name: "create_dir".into(),
description: "Create a new directory".into(),
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path where to create the directory",
},
},
"required": ["path"],
})
.as_object()
.unwrap()
.clone(),
},
ToolDescription {
name: "list_dir".into(),
description: "List contents of a directory".into(),
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the directory to list",
},
},
"required": ["path"],
})
.as_object()
.unwrap()
.clone(),
},
ToolDescription {
name: "move_file".into(),
description: "Move a file from one location to another".into(),
input_schema: json!({
"type": "object",
"properties": {
"from": {
"type": "string",
"description": "Source path of the file",
},
"to": {
"type": "string",
"description": "Destination path for the file",
},
},
"required": ["from", "to"],
})
.as_object()
.unwrap()
.clone(),
},
ToolDescription {
name: "search_files".into(),
description: "Search for files matching a pattern in a directory".into(),
input_schema: json!({
"type": "object",
"properties": {
"directory": {
"type": "string",
"description": "Directory to search in",
},
"pattern": {
"type": "string",
"description": "Pattern to match against filenames",
},
},
"required": ["directory", "pattern"],
})
.as_object()
.unwrap()
.clone(),
},
ToolDescription {
name: "get_file_info".into(),
description: "Get information about a file or directory".into(),
input_schema: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to get information about",
},
},
"required": ["path"],
})
.as_object()
.unwrap()
.clone(),
},
],
})
}
```
--------------------------------------------------------------------------------
/templates/plugins/go/types.go:
--------------------------------------------------------------------------------
```go
package main
import (
"encoding/json"
"fmt"
"time"
)
// Annotations represents metadata annotations for resources and content
type Annotations struct {
Audience []Role `json:"audience,omitempty"`
LastModified *time.Time `json:"lastModified,omitempty"`
Priority float32 `json:"priority,omitempty"`
}
// AudioContent represents audio content in a message
type AudioContent struct {
Meta Meta `json:"_meta,omitempty"`
Annotations *Annotations `json:"annotations,omitempty"`
Data string `json:"data"`
MimeType string `json:"mimeType"`
}
func (a AudioContent) MarshalJSON() ([]byte, error) {
type alias AudioContent
return json.Marshal(&struct {
Type string `json:"type"`
alias
}{
Type: "audio",
alias: (alias)(a),
})
}
func (a *AudioContent) UnmarshalJSON(data []byte) error {
type alias AudioContent
aux := struct {
Type string `json:"type"`
alias
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
// Optional: validate `type`
if aux.Type != "audio" && aux.Type != "" { // allow empty if missing
return fmt.Errorf("invalid type %q, expected \"audio\"", aux.Type)
}
*a = AudioContent(aux.alias)
return nil
}
// BlobResourceContents represents binary resource contents
type BlobResourceContents struct {
Meta Meta `json:"_meta,omitempty"`
Blob string `json:"blob"`
MimeType *string `json:"mimeType,omitempty"`
URI string `json:"uri"`
}
// BooleanSchema represents a boolean input schema
type BooleanSchema struct {
Default *bool `json:"default,omitempty"`
Description *string `json:"description,omitempty"`
Title *string `json:"title,omitempty"`
}
func (b BooleanSchema) MarshalJSON() ([]byte, error) {
type alias BooleanSchema
return json.Marshal(&struct {
Type string `json:"type"`
alias
}{
Type: "boolean",
alias: (alias)(b),
})
}
func (b *BooleanSchema) UnmarshalJSON(data []byte) error {
type alias BooleanSchema
aux := struct {
Type string `json:"type"`
alias
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
// Optional: validate `type`
if aux.Type != "boolean" && aux.Type != "" { // allow empty if missing
return fmt.Errorf("invalid type %q, expected \"boolean\"", aux.Type)
}
*b = BooleanSchema(aux.alias)
return nil
}
// CallToolRequest represents a request to call a tool
type CallToolRequest struct {
Context PluginRequestContext `json:"context"`
Request CallToolRequestParam `json:"request"`
}
// CallToolRequestParam represents parameters for calling a tool
type CallToolRequestParam struct {
Arguments map[string]any `json:"arguments,omitempty"`
Name string `json:"name"`
}
// CallToolResult represents the result of calling a tool
type CallToolResult struct {
Meta Meta `json:"_meta,omitempty"`
Content []ContentBlock `json:"content"`
IsError *bool `json:"isError,omitempty"`
StructuredContent map[string]any `json:"structuredContent,omitempty"`
}
// CompleteRequest represents a request for completion suggestions
type CompleteRequest struct {
Context PluginRequestContext `json:"context"`
Request CompleteRequestParam `json:"request"`
}
// CompleteRequestParam represents parameters for completion
type CompleteRequestParam struct {
Argument CompleteRequestParamArgument `json:"argument"`
Context *CompleteRequestParamContext `json:"context,omitempty"`
Ref Reference `json:"ref"`
}
// CompleteRequestParamArgument represents an argument for completion
type CompleteRequestParamArgument struct {
Name string `json:"name"`
Value string `json:"value"`
}
// CompleteRequestParamContext represents context for completion
type CompleteRequestParamContext struct {
Arguments map[string]string `json:"arguments,omitempty"`
}
// CompleteResult represents completion suggestions
type CompleteResult struct {
Completion CompleteResultCompletion `json:"completion"`
}
// CompleteResultCompletion represents completion values
type CompleteResultCompletion struct {
HasMore *bool `json:"hasMore,omitempty"`
Total *int64 `json:"total,omitempty"`
Values []string `json:"values"`
}
type ContentBlock struct {
Audio *AudioContent
EmbeddedResource *EmbeddedResource
Image *ImageContent
ResourceLink *ResourceLinkContent
Text *TextContent
}
func (c ContentBlock) MarshalJSON() ([]byte, error) {
switch {
case c.Audio != nil:
return json.Marshal(c.Audio)
case c.EmbeddedResource != nil:
return json.Marshal(c.EmbeddedResource)
case c.Image != nil:
return json.Marshal(c.Image)
case c.ResourceLink != nil:
return json.Marshal(c.ResourceLink)
case c.Text != nil:
return json.Marshal(c.Text)
default:
return nil, fmt.Errorf("empty ContentItem")
}
}
func (c *ContentBlock) UnmarshalJSON(data []byte) error {
var head struct {
Type string `json:"type"`
}
if err := json.Unmarshal(data, &head); err != nil {
return err
}
switch head.Type {
case "audio":
var a AudioContent
if err := json.Unmarshal(data, &a); err != nil {
return err
}
c.Audio = &a
case "resource":
var r EmbeddedResource
if err := json.Unmarshal(data, &r); err != nil {
return err
}
c.EmbeddedResource = &r
case "image":
var i ImageContent
if err := json.Unmarshal(data, &i); err != nil {
return err
}
c.Image = &i
case "resource_link":
var rl ResourceLinkContent
if err := json.Unmarshal(data, &rl); err != nil {
return err
}
c.ResourceLink = &rl
case "text":
var t TextContent
if err := json.Unmarshal(data, &t); err != nil {
return err
}
c.Text = &t
default:
return fmt.Errorf("unknown content type %q", head.Type)
}
return nil
}
// CreateMessageRequestParam represents a request to create a message
type CreateMessageRequestParam struct {
IncludeContext *CreateMessageRequestParamIncludeContext `json:"includeContext,omitempty"`
MaxTokens int64 `json:"maxTokens"`
Messages []SamplingMessage `json:"messages"`
ModelPreferences *ModelPreferences `json:"modelPreferences,omitempty"`
StopSequences []string `json:"stopSequences,omitempty"`
SystemPrompt *string `json:"systemPrompt,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
}
// CreateMessageRequestParamIncludeContext represents context inclusion options
type CreateMessageRequestParamIncludeContext string
const (
AllServers CreateMessageRequestParamIncludeContext = "allServers"
None CreateMessageRequestParamIncludeContext = "none"
ThisServer CreateMessageRequestParamIncludeContext = "thisServer"
)
func (t *CreateMessageRequestParamIncludeContext) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
ct := CreateMessageRequestParamIncludeContext(s)
if !ct.Valid() {
return fmt.Errorf("invalid CreateMessageRequestParamIncludeContext %q", s)
}
*t = ct
return nil
}
func (t CreateMessageRequestParamIncludeContext) Valid() bool {
switch t {
case AllServers, None, ThisServer:
return true
default:
return false
}
}
// CreateMessageResult represents the result of creating a message
type CreateMessageResult struct {
Content CreateMessageResultContent `json:"content"`
Model string `json:"model"`
Role Role `json:"role"`
StopReason *string `json:"stopReason,omitempty"`
}
type CreateMessageResultContent SamplingMessage
// ElicitRequestParamWithTimeout represents a request for user elicitation
type ElicitRequestParamWithTimeout struct {
Message string `json:"message"`
RequestedSchema Schema `json:"requestedSchema"`
Timeout *int64 `json:"timeout,omitempty"`
}
// ElicitResult represents the result of an elicitation
type ElicitResult struct {
Action ElicitResultAction `json:"action"`
Content map[string]ElicitResultContentValue `json:"content,omitempty"`
}
// ElicitResultAction represents the action taken in elicitation
type ElicitResultAction string
const (
Accept ElicitResultAction = "accept"
Cancel ElicitResultAction = "cancel"
Decline ElicitResultAction = "decline"
)
func (e *ElicitResultAction) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
ea := ElicitResultAction(s)
if !ea.Valid() {
return fmt.Errorf("invalid ElicitResultAction %q", s)
}
*e = ea
return nil
}
func (e ElicitResultAction) Valid() bool {
switch e {
case Accept, Cancel, Decline:
return true
default:
return false
}
}
type ElicitResultContentValue struct {
String *string
Number *json.Number
Boolean *bool
}
func (v ElicitResultContentValue) MarshalJSON() ([]byte, error) {
switch {
case v.String != nil:
return json.Marshal(v.String)
case v.Number != nil:
return json.Marshal(v.Number)
case v.Boolean != nil:
return json.Marshal(v.Boolean)
default:
return nil, fmt.Errorf("ElicitResultContentValue has no value set")
}
}
func (v *ElicitResultContentValue) UnmarshalJSON(data []byte) error {
// Clear existing values
*v = ElicitResultContentValue{}
// Try string first
var s string
if err := json.Unmarshal(data, &s); err == nil {
v.String = &s
return nil
}
// Then bool
var b bool
if err := json.Unmarshal(data, &b); err == nil {
v.Boolean = &b
return nil
}
// Then number
var n json.Number
if err := json.Unmarshal(data, &n); err == nil {
v.Number = &n
return nil
}
// If all fail, it's not a valid primitive for this type
return fmt.Errorf("ElicitResultContentValue: unsupported JSON value: %s", string(data))
}
// EmbeddedResource represents an embedded resource
type EmbeddedResource struct {
Meta Meta `json:"_meta,omitempty"`
Annotations *Annotations `json:"annotations,omitempty"`
Resource ResourceContents `json:"resource"`
}
func (e EmbeddedResource) MarshalJSON() ([]byte, error) {
type alias EmbeddedResource
return json.Marshal(&struct {
Type string `json:"type"`
alias
}{
Type: "resource",
alias: (alias)(e),
})
}
func (e *EmbeddedResource) UnmarshalJSON(data []byte) error {
type alias EmbeddedResource
aux := struct {
Type string `json:"type"`
alias
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.Type != "resource" && aux.Type != "" {
return fmt.Errorf("invalid type %q, expected \"resource\"", aux.Type)
}
*e = EmbeddedResource(aux.alias)
return nil
}
// EnumSchema represents an enum input schema
type EnumSchema struct {
Description *string `json:"description,omitempty"`
Enum []string `json:"enum"`
EnumNames []string `json:"enumNames,omitempty"`
Title *string `json:"title,omitempty"`
}
func (e EnumSchema) MarshallJSON() ([]byte, error) {
type alias EnumSchema
return json.Marshal(&struct {
Type string `json:"type"`
alias
}{
Type: "resource",
alias: (alias)(e),
})
}
func (e *EnumSchema) UnmarshalJSON(data []byte) error {
type alias EnumSchema
aux := struct {
Type string `json:"type"`
alias
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.Type != "string" && aux.Type != "" {
return fmt.Errorf("invalid type %q, expected \"string\"", aux.Type)
}
*e = EnumSchema(aux.alias)
return nil
}
// GetPromptRequest represents a request to get a prompt
type GetPromptRequest struct {
Context PluginRequestContext `json:"context"`
Request GetPromptRequestParam `json:"request"`
}
// GetPromptRequestParam represents parameters for getting a prompt
type GetPromptRequestParam struct {
Arguments map[string]string `json:"arguments,omitempty"`
Name string `json:"name"`
}
// GetPromptResult represents the result of getting a prompt
type GetPromptResult struct {
Description *string `json:"description,omitempty"`
Messages []PromptMessage `json:"messages"`
}
// ImageContent represents image content
type ImageContent struct {
Meta Meta `json:"_meta,omitempty"`
Annotations *Annotations `json:"annotations,omitempty"`
Data string `json:"data"`
MimeType string `json:"mimeType"`
}
func (i ImageContent) MarshallJSON() ([]byte, error) {
type alias ImageContent
return json.Marshal(&struct {
Type string `json:"type"`
alias
}{
Type: "image",
alias: (alias)(i),
})
}
func (i *ImageContent) UnmarshalJSON(data []byte) error {
type alias ImageContent
aux := struct {
Type string `json:"type"`
alias
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.Type != "image" && aux.Type != "" { // allow empty if missing
return fmt.Errorf("invalid type %q, expected \"image\"", aux.Type)
}
*i = ImageContent(aux.alias)
return nil
}
// ListPromptsRequest represents a request to list prompts
type ListPromptsRequest struct {
Context PluginRequestContext `json:"context"`
}
// ListPromptsResult represents the result of listing prompts
type ListPromptsResult struct {
Prompts []Prompt `json:"prompts"`
}
// ListResourcesRequest represents a request to list resources
type ListResourcesRequest struct {
Context PluginRequestContext `json:"context"`
}
// ListResourcesResult represents the result of listing resources
type ListResourcesResult struct {
Resources []Resource `json:"resources"`
}
// ListResourceTemplatesRequest represents a request to list resource templates
type ListResourceTemplatesRequest struct {
Context PluginRequestContext `json:"context"`
}
// ListResourceTemplatesResult represents the result of listing resource templates
type ListResourceTemplatesResult struct {
ResourceTemplates []ResourceTemplate `json:"resourceTemplates"`
}
// ListRootsResult represents the result of listing roots
type ListRootsResult struct {
Roots []Root `json:"roots"`
}
// ListToolsRequest represents a request to list tools
type ListToolsRequest struct {
Context PluginRequestContext `json:"context"`
}
// ListToolsResult represents the result of listing tools
type ListToolsResult struct {
Tools []Tool `json:"tools"`
}
// LoggingLevel represents the severity level of a log message
type LoggingLevel string
const (
Debug LoggingLevel = "debug"
Info LoggingLevel = "info"
Notice LoggingLevel = "notice"
Warning LoggingLevel = "warning"
Error LoggingLevel = "error"
Critical LoggingLevel = "critical"
Alert LoggingLevel = "alert"
Emergency LoggingLevel = "emergency"
)
func (l *LoggingLevel) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
ll := LoggingLevel(s)
if !ll.Validate() {
return fmt.Errorf("invalid LoggingLevel %q", s)
}
*l = ll
return nil
}
func (l LoggingLevel) Validate() bool {
switch l {
case Debug, Info, Notice, Warning, Error, Critical, Alert, Emergency:
return true
default:
return false
}
}
// LoggingMessageNotificationParam represents a logging message notification
type LoggingMessageNotificationParam struct {
Data any `json:"data"`
Level LoggingLevel `json:"level"`
Logger *string `json:"logger,omitempty"`
}
// Meta represents metadata as a generic JSON object
type Meta map[string]any
// ModelHint represents a hint for model selection
type ModelHint struct {
Name string `json:"name"`
}
// ModelPreferences represents preferences for model selection
type ModelPreferences struct {
CostPriority float32 `json:"costPriority,omitempty"`
Hints []ModelHint `json:"hints,omitempty"`
IntelligencePriority float32 `json:"intelligencePriority,omitempty"`
SpeedPriority float32 `json:"speedPriority,omitempty"`
}
// NumberSchema represents a number input schema
type NumberSchema struct {
Description *string `json:"description,omitempty"`
Maximum *float64 `json:"maximum,omitempty"`
Minimum *float64 `json:"minimum,omitempty"`
Title *string `json:"title,omitempty"`
Type NumberType `json:"type"` // "number" or "integer"
}
// NumberType represents the type of a number schema
type NumberType string
const (
Number NumberType = "number"
Integer NumberType = "integer"
)
func (n *NumberType) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
nt := NumberType(s)
if !nt.Valid() {
return fmt.Errorf("invalid NumberType %q", s)
}
*n = nt
return nil
}
func (n NumberType) Valid() bool {
switch n {
case Number, Integer:
return true
default:
return false
}
}
// PluginNotificationContext represents the context for a plugin notification
type PluginNotificationContext struct {
Meta Meta `json:"meta"`
}
// PluginRequestContext represents the context for a plugin request
type PluginRequestContext struct {
Meta Meta `json:"_meta"`
ID PluginRequestId `json:"id"`
}
type PluginRequestId struct {
String *string
Number *int64
}
func (p PluginRequestId) MarshalJSON() ([]byte, error) {
switch {
case p.String != nil:
return json.Marshal(p.String)
case p.Number != nil:
return json.Marshal(p.Number)
default:
return nil, fmt.Errorf("empty PluginRequestId")
}
}
func (p *PluginRequestId) UnmarshalJSON(data []byte) error {
*p = PluginRequestId{}
// Try string first
var s string
if err := json.Unmarshal(data, &s); err == nil {
p.String = &s
return nil
}
// Then number
var n int64
if err := json.Unmarshal(data, &n); err == nil {
p.Number = &n
return nil
}
// If all fail, it's not a valid primitive for this type
return fmt.Errorf("PluginRequestId: unsupported JSON value: %s", string(data))
}
// PrimitiveSchemaDefinition is a union type for schema definitions
type PrimitiveSchemaDefinition struct {
Boolean *BooleanSchema
Enum *EnumSchema
Number *NumberSchema
String *StringSchema
}
func (p PrimitiveSchemaDefinition) MarshalJSON() ([]byte, error) {
switch {
case p.Boolean != nil:
return json.Marshal(p.Boolean)
case p.Enum != nil:
return json.Marshal(p.Enum)
case p.Number != nil:
return json.Marshal(p.Number)
case p.String != nil:
return json.Marshal(p.String)
default:
return nil, fmt.Errorf("empty PrimitiveSchemaDefinition")
}
}
func (p *PrimitiveSchemaDefinition) UnmarshalJSON(data []byte) error {
var head struct {
Type string `json:"type"`
}
if err := json.Unmarshal(data, &head); err != nil {
return err
}
switch head.Type {
case "boolean":
var b BooleanSchema
if err := json.Unmarshal(data, &b); err != nil {
return err
}
p.Boolean = &b
case "string":
var e EnumSchema
if err := json.Unmarshal(data, &e); err != nil {
var s StringSchema
if err := json.Unmarshal(data, &s); err != nil {
return err
}
p.String = &s
} else {
p.Enum = &e
}
case "number", "integer":
var n NumberSchema
if err := json.Unmarshal(data, &n); err != nil {
return err
}
p.Number = &n
}
return nil
}
// ProgressNotificationParam represents a progress notification
type ProgressNotificationParam struct {
Message *string `json:"message,omitempty"`
Progress float64 `json:"progress"`
ProgressToken string `json:"progressToken"`
Total *float64 `json:"total,omitempty"`
}
// Prompt represents a prompt
type Prompt struct {
Arguments []PromptArgument `json:"arguments,omitempty"`
Description *string `json:"description,omitempty"`
Name string `json:"name"`
Title *string `json:"title,omitempty"`
}
// PromptArgument represents an argument for a prompt
type PromptArgument struct {
Description *string `json:"description,omitempty"`
Name string `json:"name"`
Required *bool `json:"required,omitempty"`
Title *string `json:"title,omitempty"`
}
// PromptMessage represents a message in a prompt
type PromptMessage struct {
Content ContentBlock `json:"content"`
Role Role `json:"role"`
}
// PromptReference represents a reference to a prompt
type PromptReference struct {
Name string `json:"name"`
Title *string `json:"title,omitempty"`
}
func (p PromptReference) MarshalJSON() ([]byte, error) {
type alias PromptReference
return json.Marshal(&struct {
Type string `json:"type"`
alias
}{
Type: "prompt",
alias: (alias)(p),
})
}
func (p *PromptReference) UnmarshalJSON(data []byte) error {
type alias PromptReference
aux := struct {
Type string `json:"type"`
alias
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.Type != "prompt" && aux.Type != "" { // allow empty if missing
return fmt.Errorf("invalid type %q, expected \"prompt\"", aux.Type)
}
*p = PromptReference(aux.alias)
return nil
}
// ReadResourceRequest represents a request to read a resource
type ReadResourceRequest struct {
Context PluginRequestContext `json:"context"`
Request ReadResourceRequestParam `json:"request"`
}
// ReadResourceRequestParam represents parameters for reading a resource
type ReadResourceRequestParam struct {
URI string `json:"uri"`
}
// ReadResourceResult represents the result of reading a resource
type ReadResourceResult struct {
Contents []ResourceContents `json:"contents"`
}
type Reference struct {
Prompt *PromptReference
ResourceTemplate *ResourceTemplateReference
}
func (r Reference) MarshalJSON() ([]byte, error) {
switch {
case r.Prompt != nil:
return json.Marshal(r.Prompt)
case r.ResourceTemplate != nil:
return json.Marshal(r.ResourceTemplate)
default:
return nil, fmt.Errorf("empty Reference")
}
}
func (r *Reference) UnmarshalJSON(data []byte) error {
var head struct {
Type string `json:"type"`
}
if err := json.Unmarshal(data, &head); err != nil {
return err
}
switch head.Type {
case "prompt":
var p PromptReference
if err := json.Unmarshal(data, &p); err != nil {
return err
}
r.Prompt = &p
case "resource":
var rt ResourceTemplateReference
if err := json.Unmarshal(data, &rt); err != nil {
return err
}
r.ResourceTemplate = &rt
default:
return fmt.Errorf("unknown reference type %q", head.Type)
}
return nil
}
// Resource represents a resource
type Resource struct {
Annotations *Annotations `json:"annotations,omitempty"`
Description *string `json:"description,omitempty"`
MimeType *string `json:"mimeType,omitempty"`
Name string `json:"name"`
Size *int64 `json:"size,omitempty"`
Title *string `json:"title,omitempty"`
URI string `json:"uri"`
}
type ResourceContents struct {
Blob *BlobResourceContents
Text *TextResourceContents
}
func (R ResourceContents) MarshalJSON() ([]byte, error) {
switch {
case R.Blob != nil:
return json.Marshal(R.Blob)
case R.Text != nil:
return json.Marshal(R.Text)
default:
return nil, fmt.Errorf("empty ResourceContents")
}
}
func (r *ResourceContents) UnmarshalJSON(data []byte) error {
// Clear existing values
*r = ResourceContents{}
// Try blob first
var b BlobResourceContents
if err := json.Unmarshal(data, &b); err == nil {
r.Blob = &b
return nil
}
// Then text
var t TextResourceContents
if err := json.Unmarshal(data, &t); err == nil {
r.Text = &t
return nil
}
// If all fail, it's not a valid ResourceContents
return fmt.Errorf("ResourceContents: unsupported JSON value: %s", string(data))
}
// ResourceLinkContent represents a link to a resource
type ResourceLinkContent struct {
Meta Meta `json:"_meta,omitempty"`
Annotations *Annotations `json:"annotations,omitempty"`
Description *string `json:"description,omitempty"`
MimeType *string `json:"mimeType,omitempty"`
Name string `json:"name"`
Size *int64 `json:"size,omitempty"`
Title *string `json:"title,omitempty"`
URI string `json:"uri"`
}
func (r ResourceLinkContent) MarshallJSON() ([]byte, error) {
type alias ResourceLinkContent
return json.Marshal(&struct {
Type string `json:"type"`
alias
}{
Type: "resource_link",
alias: (alias)(r),
})
}
func (r *ResourceLinkContent) UnmarshalJSON(data []byte) error {
type alias ResourceLinkContent
aux := struct {
Type string `json:"type"`
alias
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.Type != "resource_link" && aux.Type != "" { // allow empty if missing
return fmt.Errorf("invalid type %q, expected \"resource_link\"", aux.Type)
}
*r = ResourceLinkContent(aux.alias)
return nil
}
// ResourceTemplate represents a resource template
type ResourceTemplate struct {
Annotations *Annotations `json:"annotations,omitempty"`
Description *string `json:"description,omitempty"`
MimeType *string `json:"mimeType,omitempty"`
Name string `json:"name"`
Title *string `json:"title,omitempty"`
URITemplate string `json:"uriTemplate"`
}
// ResourceTemplateReference represents a reference to a resource template
type ResourceTemplateReference struct {
URI string `json:"uri"`
}
func (r ResourceTemplateReference) MarshallJSON() ([]byte, error) {
type alias ResourceTemplateReference
return json.Marshal(&struct {
Type string `json:"type"`
alias
}{
Type: "resource",
alias: (alias)(r),
})
}
func (r *ResourceTemplateReference) UnmarshalJSON(data []byte) error {
type alias ResourceTemplateReference
aux := struct {
Type string `json:"type"`
alias
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.Type != "resource" && aux.Type != "" { // allow empty if missing
return fmt.Errorf("invalid type %q, expected \"resource\"", aux.Type)
}
*r = ResourceTemplateReference(aux.alias)
return nil
}
// ResourceUpdatedNotificationParam represents a resource update notification
type ResourceUpdatedNotificationParam struct {
URI string `json:"uri"`
}
// Role represents the role of a message sender
type Role string
const (
Assistant Role = "assistant"
User Role = "user"
)
func (r *Role) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
rr := Role(s)
if !rr.Valid() {
return fmt.Errorf("invalid Role %q", s)
}
*r = rr
return nil
}
func (r Role) Valid() bool {
switch r {
case Assistant, User:
return true
default:
return false
}
}
// Root represents a root directory or resource
type Root struct {
Name *string `json:"name,omitempty"`
URI string `json:"uri"`
}
type SamplingMessage struct {
Audio *AudioContent
Image *ImageContent
Text *TextContent
}
func (s SamplingMessage) MarshalJSON() ([]byte, error) {
switch {
case s.Audio != nil:
return json.Marshal(s.Audio)
case s.Image != nil:
return json.Marshal(s.Image)
case s.Text != nil:
return json.Marshal(s.Text)
default:
return nil, fmt.Errorf("empty SamplingMessage")
}
}
func (s *SamplingMessage) UnmarshalJSON(data []byte) error {
var head struct {
Type string `json:"type"`
}
if err := json.Unmarshal(data, &head); err != nil {
return err
}
switch head.Type {
case "audio":
var a AudioContent
if err := json.Unmarshal(data, &a); err != nil {
return err
}
s.Audio = &a
case "image":
var i ImageContent
if err := json.Unmarshal(data, &i); err != nil {
return err
}
s.Image = &i
case "text":
var t TextContent
if err := json.Unmarshal(data, &t); err != nil {
return err
}
s.Text = &t
default:
return fmt.Errorf("unknown content type %q", head.Type)
}
return nil
}
// Schema represents a JSON schema
type Schema struct {
Properties map[string]PrimitiveSchemaDefinition `json:"properties,omitempty"`
Required []string `json:"required,omitempty"`
}
func (s Schema) MarshallJSON() ([]byte, error) {
type alias Schema
return json.Marshal(&struct {
Type string `json:"type"`
alias
}{
Type: "object",
alias: (alias)(s),
})
}
func (s *Schema) UnmarshalJSON(data []byte) error {
type alias Schema
aux := struct {
Type string `json:"type"`
alias
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
// Optional: validate `type`
if aux.Type != "object" && aux.Type != "" { // allow empty if missing
return fmt.Errorf("invalid type %q, expected \"object\"", aux.Type)
}
*s = Schema(aux.alias)
return nil
}
// StringSchema represents a string input schema
type StringSchema struct {
Description *string `json:"description,omitempty"`
Format *StringSchemaFormat `json:"format,omitempty"`
MaxLength *int64 `json:"maxLength,omitempty"`
MinLength *int64 `json:"minLength,omitempty"`
Title *string `json:"title,omitempty"`
}
func (s StringSchema) MarshallJSON() ([]byte, error) {
type alias StringSchema
return json.Marshal(&struct {
Type string `json:"type"`
alias
}{
Type: "string",
alias: (alias)(s),
})
}
func (s *StringSchema) UnmarshalJSON(data []byte) error {
type alias StringSchema
aux := struct {
Type string `json:"type"`
alias
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
// Optional: validate `type`
if aux.Type != "string" && aux.Type != "" { // allow empty if missing
return fmt.Errorf("invalid type %q, expected \"string\"", aux.Type)
}
*s = StringSchema(aux.alias)
return nil
}
// StringSchemaFormat represents the format of a string schema
type StringSchemaFormat string
const (
Email StringSchemaFormat = "email"
URI StringSchemaFormat = "uri"
Date StringSchemaFormat = "date"
DateTime StringSchemaFormat = "date_time"
)
func (s *StringSchemaFormat) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
sf := StringSchemaFormat(str)
if !sf.Valid() {
return fmt.Errorf("invalid StringSchemaFormat %q", str)
}
*s = sf
return nil
}
func (s StringSchemaFormat) Valid() bool {
switch s {
case Email, URI, Date, DateTime:
return true
default:
return false
}
}
// TextContent represents text content
type TextContent struct {
Meta Meta `json:"_meta,omitempty"`
Annotations *Annotations `json:"annotations,omitempty"`
Text string `json:"text"`
}
func (t TextContent) MarshallJSON() ([]byte, error) {
type alias TextContent
return json.Marshal(&struct {
Type string `json:"type"`
alias
}{
Type: "text",
alias: (alias)(t),
})
}
func (t *TextContent) UnmarshalJSON(data []byte) error {
type alias TextContent
aux := struct {
Type string `json:"type"`
alias
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
if aux.Type != "text" && aux.Type != "" { // allow empty if missing
return fmt.Errorf("invalid type %q, expected \"text\"", aux.Type)
}
*t = TextContent(aux.alias)
return nil
}
// TextResourceContents represents text resource contents
type TextResourceContents struct {
Meta Meta `json:"_meta,omitempty"`
MimeType *string `json:"mimeType,omitempty"`
Text string `json:"text"`
URI string `json:"uri"`
}
// Tool represents a tool
type Tool struct {
Annotations *Annotations `json:"annotations,omitempty"`
Description *string `json:"description,omitempty"`
InputSchema ToolSchema `json:"inputSchema"`
Name string `json:"name"`
OutputSchema *ToolSchema `json:"outputSchema,omitempty"`
Title *string `json:"title,omitempty"`
}
// ToolSchema represents the schema for tool input or output
type ToolSchema struct {
Properties map[string]any `json:"properties,omitempty"`
Required []string `json:"required,omitempty"`
Type string `json:"type"` // "object"
}
```
--------------------------------------------------------------------------------
/examples/plugins/v2/rstime/src/pdk/types.rs:
--------------------------------------------------------------------------------
```rust
#![allow(unused)]
use base64::engine::general_purpose::STANDARD;
use base64_serde::base64_serde_type;
use extism_pdk::{FromBytes, Json, ToBytes};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Number, Value};
use std::collections::HashMap;
base64_serde_type!(Base64Standard, STANDARD);
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct Annotations {
/// Intended audience for the resource
#[serde(rename = "audience")]
pub audience: Vec<Role>,
/// Last modified timestamp for the resource
#[serde(rename = "lastModified")]
pub last_modified: chrono::DateTime<chrono::Utc>,
/// Priority level indicating the importance of the resource
#[serde(rename = "priority")]
pub priority: f32,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct AudioContent {
/// Optional additional metadata about the content block
#[serde(rename = "_meta")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub meta: Option<Meta>,
/// Optional content annotations
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<Annotations>,
/// Base64-encoded audio data
#[serde(rename = "data")]
pub data: String,
/// MIME type of the audio (e.g. 'audio/mpeg')
#[serde(rename = "mimeType")]
pub mime_type: String,
#[serde(rename = "type")]
pub r#type: AudioType,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum AudioType {
#[default]
#[serde(rename = "audio")]
Audio,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct BlobResourceContents {
/// Optional additional metadata about the blob resource
#[serde(rename = "_meta")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub meta: Option<Meta>,
/// Base64-encoded binary data of the resource
#[serde(rename = "blob")]
pub blob: String,
/// MIME type of the binary content (e.g. 'application/pdf')
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// URI of the resource
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct BooleanSchema {
/// Optional default value
#[serde(rename = "default")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub default: Option<bool>,
/// Description of the boolean input
#[serde(rename = "description")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub description: Option<String>,
/// Optional human-readable title
#[serde(rename = "title")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub title: Option<String>,
#[serde(rename = "type")]
pub r#type: BooleanType,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum BooleanType {
#[default]
#[serde(rename = "boolean")]
Boolean,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct CallToolRequest {
#[serde(rename = "context")]
pub context: PluginRequestContext,
#[serde(rename = "request")]
pub request: CallToolRequestParam,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct CallToolRequestParam {
/// Arguments to pass to the tool
#[serde(rename = "arguments")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub arguments: Option<Map<String, Value>>,
/// The name of the tool to call
#[serde(rename = "name")]
pub name: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct CallToolResult {
/// Optional additional metadata about the tool call result
#[serde(rename = "_meta")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub meta: Option<Meta>,
/// Array of TextContent, ImageContent, AudioContent, EmbeddedResource, or ResourceLinks representing the result
#[serde(rename = "content")]
pub content: Vec<ContentBlock>,
/// Whether the tool call ended in an error. If not set, defaults to false.
#[serde(rename = "isError")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub is_error: Option<bool>,
/// Optional structured JSON result from the tool
#[serde(rename = "structuredContent")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub structured_content: Option<Map<String, Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct CompleteRequest {
#[serde(rename = "context")]
pub context: PluginRequestContext,
#[serde(rename = "request")]
pub request: CompleteRequestParam,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct CompleteRequestParam {
#[serde(rename = "argument")]
pub argument: CompleteRequestParamArgument,
/// Optional completion context with previously-resolved arguments
#[serde(rename = "context")]
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<CompleteRequestParamContext>,
/// Reference to either a PromptReference or ResourceTemplateReference
#[serde(rename = "ref")]
pub r#ref: Reference,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct CompleteRequestParamArgument {
/// Name of the argument
#[serde(rename = "name")]
pub name: String,
/// Current value to complete
#[serde(rename = "value")]
pub value: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct CompleteRequestParamContext {
/// Previously-resolved argument values
#[serde(rename = "arguments")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub arguments: Option<HashMap<String, String>>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct CompleteResult {
#[serde(rename = "completion")]
pub completion: CompleteResultCompletion,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct CompleteResultCompletion {
/// Whether there are more completions available
#[serde(rename = "hasMore")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub has_more: Option<bool>,
/// Total number of available completions
#[serde(rename = "total")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub total: Option<i64>,
/// Array of completion values (max 100 items)
#[serde(rename = "values")]
pub values: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
#[serde(untagged)]
pub enum ContentBlock {
Audio(AudioContent),
EmbeddedResource(EmbeddedResource),
Image(ImageContent),
ResourceLink(ResourceLink),
Text(TextContent),
Empty(Empty),
}
impl Default for ContentBlock {
fn default() -> Self {
ContentBlock::Empty(Empty::default())
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct CreateMessageRequestParam {
#[serde(rename = "includeContext")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub include_context: Option<CreateMessageRequestParamIncludeContext>,
/// Maximum tokens to sample
#[serde(rename = "maxTokens")]
pub max_tokens: i64,
/// Conversation messages of of TextContent, ImageContent or AudioContent
#[serde(rename = "messages")]
pub messages: Vec<SamplingMessage>,
/// Preferences for model selection
#[serde(rename = "modelPreferences")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub model_preferences: Option<ModelPreferences>,
/// Stop sequences
#[serde(rename = "stopSequences")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub stop_sequences: Option<Vec<String>>,
/// Optional system prompt
#[serde(rename = "systemPrompt")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub system_prompt: Option<String>,
/// Sampling temperature
#[serde(rename = "temperature")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub temperature: Option<f64>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum CreateMessageRequestParamIncludeContext {
#[default]
#[serde(rename = "none")]
None,
#[serde(rename = "thisServer")]
ThisServer,
#[serde(rename = "allServers")]
AllServers,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct CreateMessageResult {
/// One of TextContent, ImageContent or AudioContent
#[serde(rename = "content")]
pub content: CreateMessageResultContent,
/// Name of the model used
#[serde(rename = "model")]
pub model: String,
#[serde(rename = "role")]
pub role: Role,
/// Optional reason sampling stopped
#[serde(rename = "stopReason")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub stop_reason: Option<String>,
}
type CreateMessageResultContent = SamplingMessage;
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ElicitRequestParamWithTimeout {
/// Message to present to the user
#[serde(rename = "message")]
pub message: String,
#[serde(rename = "requestedSchema")]
pub requested_schema: Schema,
/// Optional timeout in milliseconds
#[serde(rename = "timeout")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub timeout: Option<i64>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ElicitResult {
#[serde(rename = "action")]
pub action: ElicitResultAction,
/// Form data submitted by user (only present when action is accept)
#[serde(rename = "content")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub content: Option<HashMap<String, ElicitResultContentValue>>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum ElicitResultAction {
#[default]
#[serde(rename = "accept")]
Accept,
#[serde(rename = "decline")]
Decline,
#[serde(rename = "cancel")]
Cancel,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
#[serde(untagged)]
pub enum ElicitResultContentValue {
String(String),
Number(Number), // or serde_json::Number if you want exactness
Bool(bool),
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct EmbeddedResource {
/// Optional additional metadata about the embedded resource
#[serde(rename = "_meta")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub meta: Option<Meta>,
/// Optional resource annotations
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<Annotations>,
/// The embedded TextResourceContents or BlobResourceContents
#[serde(rename = "resource")]
pub resource: ResourceContents,
#[serde(rename = "type")]
pub r#type: ResourceType,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Empty {}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct EnumSchema {
/// Description of the enum input
#[serde(rename = "description")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub description: Option<String>,
/// Array of allowed string values
#[serde(rename = "enum")]
pub r#enum: Vec<String>,
/// Optional array of human-readable names for the enum values
#[serde(rename = "enumNames")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub enum_names: Option<Vec<String>>,
/// Optional human-readable title
#[serde(rename = "title")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub title: Option<String>,
#[serde(rename = "type")]
pub r#type: StringType,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct GetPromptRequest {
#[serde(rename = "context")]
pub context: PluginRequestContext,
#[serde(rename = "request")]
pub request: GetPromptRequestParam,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct GetPromptRequestParam {
/// Arguments for templating the prompt
#[serde(rename = "arguments")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub arguments: Option<HashMap<String, String>>,
/// Name of the prompt to retrieve
#[serde(rename = "name")]
pub name: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct GetPromptResult {
/// Optional description of the prompt
#[serde(rename = "description")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub description: Option<String>,
/// Array of prompt messages
#[serde(rename = "messages")]
pub messages: Vec<PromptMessage>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ImageContent {
/// Optional additional metadata about the content block
#[serde(rename = "_meta")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub meta: Option<Meta>,
/// Optional content annotations
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<Annotations>,
/// Base64-encoded image data
#[serde(rename = "data")]
pub data: String,
/// MIME type of the image (e.g. 'image/png')
#[serde(rename = "mimeType")]
pub mime_type: String,
#[serde(rename = "type")]
pub r#type: ImageType,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum ImageType {
#[default]
#[serde(rename = "image")]
Image,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ListPromptsRequest {
#[serde(rename = "context")]
pub context: PluginRequestContext,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ListPromptsResult {
/// Array of available prompts
#[serde(rename = "prompts")]
pub prompts: Vec<Prompt>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ListResourcesRequest {
#[serde(rename = "context")]
pub context: PluginRequestContext,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ListResourcesResult {
/// Array of available resources
#[serde(rename = "resources")]
pub resources: Vec<Resource>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ListResourceTemplatesRequest {
#[serde(rename = "context")]
pub context: PluginRequestContext,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ListResourceTemplatesResult {
/// Array of resource templates
#[serde(rename = "resourceTemplates")]
pub resource_templates: Vec<ResourceTemplate>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ListRootsResult {
/// Array of root directories/resources
#[serde(rename = "roots")]
pub roots: Vec<Root>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ListToolsRequest {
#[serde(rename = "context")]
pub context: PluginRequestContext,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ListToolsResult {
/// Array of available tools
#[serde(rename = "tools")]
pub tools: Vec<Tool>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum LoggingLevel {
#[default]
#[serde(rename = "debug")]
Debug,
#[serde(rename = "info")]
Info,
#[serde(rename = "notice")]
Notice,
#[serde(rename = "warning")]
Warning,
#[serde(rename = "error")]
Error,
#[serde(rename = "critical")]
Critical,
#[serde(rename = "alert")]
Alert,
#[serde(rename = "emergency")]
Emergency,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct LoggingMessageNotificationParam {
/// Data to log (any JSON-serializable type)
#[serde(rename = "data")]
pub data: Value,
#[serde(rename = "level")]
pub level: LoggingLevel,
/// Optional logger name
#[serde(rename = "logger")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub logger: Option<String>,
}
type Meta = Map<String, Value>;
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ModelHint {
/// Suggested model name or family
#[serde(rename = "name")]
pub name: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ModelPreferences {
/// Priority for cost (0-1)
#[serde(rename = "costPriority")]
pub cost_priority: f32,
/// Model name hints
#[serde(rename = "hints")]
pub hints: Vec<ModelHint>,
/// Priority for intelligence (0-1)
#[serde(rename = "intelligencePriority")]
pub intelligence_priority: f32,
/// Priority for speed (0-1)
#[serde(rename = "speedPriority")]
pub speed_priority: f32,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct NumberSchema {
/// Description of the number input
#[serde(rename = "description")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub description: Option<String>,
/// Maximum value
#[serde(rename = "maximum")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub maximum: Option<f64>,
/// Minimum value
#[serde(rename = "minimum")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub minimum: Option<f64>,
/// Optional human-readable title
#[serde(rename = "title")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub title: Option<String>,
#[serde(rename = "type")]
pub r#type: NumberType,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum NumberType {
#[default]
#[serde(rename = "number")]
Number,
#[serde(rename = "integer")]
Integer,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum ObjectType {
#[default]
#[serde(rename = "object")]
Object,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct PluginNotificationContext {
/// Additional metadata about the notification
#[serde(rename = "meta")]
pub meta: Meta,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct PluginRequestContext {
/// Additional metadata about the request
#[serde(rename = "_meta")]
pub meta: Meta,
/// Unique identifier for this request
#[serde(rename = "id")]
pub id: PluginRequestId,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
#[serde(untagged)]
pub enum PluginRequestId {
String(String),
Number(i64),
}
impl Default for PluginRequestId {
fn default() -> Self {
PluginRequestId::String(String::new())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
#[serde(untagged)]
pub enum PrimitiveSchemaDefinition {
Boolean(BooleanSchema),
Enum(EnumSchema),
Number(NumberSchema),
String(StringSchema),
Empty(Empty),
}
impl Default for PrimitiveSchemaDefinition {
fn default() -> Self {
PrimitiveSchemaDefinition::Empty(Empty::default())
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ProgressNotificationParam {
/// Optional progress message describing current operation
#[serde(rename = "message")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub message: Option<String>,
/// The progress thus far
#[serde(rename = "progress")]
pub progress: f64,
/// A token identifying the progress context
#[serde(rename = "progressToken")]
pub progress_token: String,
/// Optional total units of work
#[serde(rename = "total")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub total: Option<f64>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct Prompt {
/// Optional prompt arguments
#[serde(rename = "arguments")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub arguments: Option<Vec<PromptArgument>>,
/// Description of what the prompt does
#[serde(rename = "description")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub description: Option<String>,
/// Unique name of the prompt
#[serde(rename = "name")]
pub name: String,
/// Human-readable title
#[serde(rename = "title")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub title: Option<String>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct PromptArgument {
/// Description of the argument
#[serde(rename = "description")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub description: Option<String>,
/// Name of the argument
#[serde(rename = "name")]
pub name: String,
/// Whether this argument is required
#[serde(rename = "required")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub required: Option<bool>,
/// Human-readable title
#[serde(rename = "title")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub title: Option<String>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct PromptMessage {
/// One of TextContent, ImageContent, AudioContent, EmbeddedResource, or ResourceLink
#[serde(rename = "content")]
pub content: ContentBlock,
#[serde(rename = "role")]
pub role: Role,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct PromptReference {
/// Name of the prompt
#[serde(rename = "name")]
pub name: String,
/// Optional human-readable title
#[serde(rename = "title")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub title: Option<String>,
#[serde(rename = "type")]
pub r#type: PromptReferenceType,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum PromptReferenceType {
#[default]
#[serde(rename = "prompt")]
Prompt,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ReadResourceRequest {
#[serde(rename = "context")]
pub context: PluginRequestContext,
#[serde(rename = "request")]
pub request: ReadResourceRequestParam,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ReadResourceRequestParam {
/// URI of the resource to read
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ReadResourceResult {
/// Array of TextResourceContents or BlobResourceContents
#[serde(rename = "contents")]
pub contents: Vec<ResourceContents>,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
#[serde(untagged)]
pub enum Reference {
Prompt(PromptReference),
ResourceTemplate(ResourceTemplateReference),
Empty(Empty),
}
impl Default for Reference {
fn default() -> Self {
Reference::Empty(Empty::default())
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct Resource {
/// Optional resource annotations
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<Annotations>,
/// Description of the resource
#[serde(rename = "description")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub description: Option<String>,
/// MIME type of the resource
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// Human-readable name
#[serde(rename = "name")]
pub name: String,
/// Size in bytes
#[serde(rename = "size")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub size: Option<i64>,
/// Human-readable title
#[serde(rename = "title")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub title: Option<String>,
/// URI of the resource
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
#[serde(untagged)]
pub enum ResourceContents {
Blob(BlobResourceContents),
Text(TextResourceContents),
Empty(Empty),
}
impl Default for ResourceContents {
fn default() -> Self {
ResourceContents::Empty(Empty::default())
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ResourceLink {
/// Optional additional metadata about the resource link
#[serde(rename = "_meta")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub meta: Option<Meta>,
/// Optional resource annotations
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<Annotations>,
/// Optional description of the resource
#[serde(rename = "description")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub description: Option<String>,
/// Optional MIME type of the resource
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// Optional human-readable name
#[serde(rename = "name")]
pub name: String,
/// Optional size in bytes
#[serde(rename = "size")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub size: Option<i64>,
/// Optional human-readable title
#[serde(rename = "title")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub title: Option<String>,
#[serde(rename = "type")]
pub r#type: ResourceLinkType,
/// URI of the resource
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum ResourceLinkType {
#[default]
#[serde(rename = "resource_link")]
ResourceLink,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum ResourceReferenceType {
#[default]
#[serde(rename = "resource")]
Resource,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ResourceTemplate {
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<Annotations>,
/// Description of the template
#[serde(rename = "description")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub description: Option<String>,
/// MIME type for resources matching this template
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// Human-readable name
#[serde(rename = "name")]
pub name: String,
/// Human-readable title
#[serde(rename = "title")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub title: Option<String>,
/// RFC 6570 URI template pattern
#[serde(rename = "uriTemplate")]
pub uri_template: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ResourceTemplateReference {
#[serde(rename = "type")]
pub r#type: ResourceReferenceType,
/// URI or URI template pattern of the resource
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum ResourceType {
#[default]
#[serde(rename = "resource")]
Resource,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ResourceUpdatedNotificationParam {
/// URI of the updated resource
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum Role {
#[default]
#[serde(rename = "assistant")]
Assistant,
#[serde(rename = "user")]
User,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct Root {
/// Optional human-readable name
#[serde(rename = "name")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub name: Option<String>,
/// URI of the root (typically file://)
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
#[serde(untagged)]
pub enum SamplingMessage {
Audio(AudioContent),
Image(ImageContent),
Text(TextContent),
Empty(Empty),
}
impl Default for SamplingMessage {
fn default() -> Self {
SamplingMessage::Empty(Empty::default())
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct Schema {
/// A map of StringSchema, NumberSchema, BooleanSchema or EnumSchema definitions (no nesting)
#[serde(rename = "properties")]
pub properties: HashMap<String, PrimitiveSchemaDefinition>,
/// Required property names
#[serde(rename = "required")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub required: Option<Vec<String>>,
#[serde(rename = "type")]
pub r#type: ObjectType,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct StringSchema {
/// Description of the string input
#[serde(rename = "description")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub description: Option<String>,
#[serde(rename = "format")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub format: Option<StringSchemaFormat>,
/// Maximum length of the string
#[serde(rename = "maxLength")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub max_length: Option<i64>,
/// Minimum length of the string
#[serde(rename = "minLength")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub min_length: Option<i64>,
/// Optional human-readable title
#[serde(rename = "title")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub title: Option<String>,
#[serde(rename = "type")]
pub r#type: StringType,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum StringSchemaFormat {
#[default]
#[serde(rename = "email")]
Email,
#[serde(rename = "uri")]
Uri,
#[serde(rename = "date")]
Date,
#[serde(rename = "date_time")]
Datetime,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum StringType {
#[default]
#[serde(rename = "string")]
String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct TextContent {
/// Optional additional metadata about the content block
#[serde(rename = "_meta")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub meta: Option<Meta>,
/// Optional content annotations
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<Annotations>,
/// The text content
#[serde(rename = "text")]
pub text: String,
#[serde(rename = "type")]
pub r#type: TextType,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct TextResourceContents {
/// Optional additional metadata about the text resource
#[serde(rename = "_meta")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub meta: Option<Meta>,
/// MIME type of the text content (e.g. 'text/plain')
#[serde(rename = "mimeType")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub mime_type: Option<String>,
/// Text content of the resource
#[serde(rename = "text")]
pub text: String,
/// URI of the resource
#[serde(rename = "uri")]
pub uri: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub enum TextType {
#[default]
#[serde(rename = "text")]
Text,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct Tool {
/// Optional tool annotations
#[serde(rename = "annotations")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub annotations: Option<Annotations>,
/// Description of what the tool does
#[serde(rename = "description")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub description: Option<String>,
#[serde(rename = "inputSchema")]
pub input_schema: ToolSchema,
/// Unique name of the tool
#[serde(rename = "name")]
pub name: String,
#[serde(rename = "outputSchema")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub output_schema: Option<ToolSchema>,
/// Human-readable title
#[serde(rename = "title")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub title: Option<String>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, FromBytes, ToBytes)]
#[encoding(Json)]
pub struct ToolSchema {
/// Schema properties
#[serde(rename = "properties")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub properties: Option<Map<String, Value>>,
/// Required properties
#[serde(rename = "required")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub required: Option<Vec<String>>,
#[serde(rename = "type")]
pub r#type: ObjectType,
}
```