Skip to content

Commit 62b98b9

Browse files
authored
fetch: split fetch-image from fetch, brave image search (#64)
* fetch: split fetch-image from fetch, brave image search Resources currently only work well in interactive use cases, they break as tasks, so let us spin off fetch-image into its own servlet Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
1 parent b6f87ee commit 62b98b9

File tree

12 files changed

+595
-108
lines changed

12 files changed

+595
-108
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
target
1+
target
2+
.idea/

servlets/brave-search/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212

1313
- `brave-web-search` searches the Web for content (text, image, video, etc...).
1414
Generally suitable for text-based searches.
15-
- `brave-image-search` to fetch pictures. Suggested use combined with the fetch tool,
16-
which includes a generic image fetch tool
15+
- `brave-image-search` to search specifically for pictures.
1716

1817
## Example
1918

servlets/brave-search/main.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ func loadKeys() {
2525
apiKey = k
2626
}
2727

28-
// Called when the tool is invoked.
29-
// If you support multiple tools, you must switch on the input.params.name to detect which tool is being called.
30-
// It takes CallToolRequest as input (The incoming tool request from the LLM)
31-
// And returns CallToolResult (The servlet's response to the given tool call)
3228
func Call(input CallToolRequest) (CallToolResult, error) {
3329
loadKeys()
3430
args := args{input.Params.Arguments.(map[string]any)}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[build]
2+
target = "wasm32-wasip1"

servlets/fetch-image/.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Cargo
2+
# will have compiled files and executables
3+
debug/
4+
target/
5+
6+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
7+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
8+
Cargo.lock
9+
10+
# These are backup files generated by rustfmt
11+
**/*.rs.bk
12+
13+
# MSVC Windows builds of rustc generate these, which store debugging information
14+
*.pdb
15+
16+
# RustRover
17+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
18+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
19+
# and can be added to the global gitignore or merged into this file. For a more nuclear
20+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
21+
#.idea/

servlets/fetch-image/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "fetch"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[lib]
7+
name = "plugin"
8+
crate-type = ["cdylib"]
9+
10+
[dependencies]
11+
#extism-pdk = "1.3.0"
12+
extism-pdk = "=1.2.1"
13+
chrono = { version = "0.4", features = ["serde"] }
14+
serde = { version = "1.0", features = ["derive"] }
15+
serde_json = "1.0"
16+
base64-serde = "0.7"
17+
base64 = "0.21"
18+
htmd = "0.1.6"

servlets/fetch-image/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Fetch Image Servlet
2+
3+
A servlet that fetches pictures and returns them as resources. Intended for interactive use (e.g. in Claude Desktop).
4+
5+
## What it does
6+
7+
Takes a URL, fetches the image and return it base64-encoded for inline-display.
8+
9+
## Usage
10+
11+
Call with:
12+
```typescript
13+
{
14+
`arguments`: {
15+
`url`: `https://example.com`, // Required: URL to fetch
16+
`mime-type`: "image/png" // The mime type to filter by
17+
}
18+
}
19+
```

servlets/fetch-image/prepare.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/bash
2+
set -eou pipefail
3+
4+
# Function to check if a command exists
5+
command_exists () {
6+
command -v "$1" >/dev/null 2>&1
7+
}
8+
9+
missing_deps=0
10+
11+
# Check for Cargo
12+
if ! (command_exists cargo); then
13+
missing_deps=1
14+
echo "❌ Cargo/rust is not installed."
15+
echo ""
16+
echo "To install Rust, visit the official download page:"
17+
echo "👉 https://www.rust-lang.org/tools/install"
18+
echo ""
19+
echo "Or install it using a package manager:"
20+
echo ""
21+
echo "🔹 macOS (Homebrew):"
22+
echo " brew install cargo"
23+
echo ""
24+
echo "🔹 Ubuntu/Debian:"
25+
echo " sudo apt-get install -y cargo"
26+
echo ""
27+
echo "🔹 Arch Linux:"
28+
echo " sudo pacman -S rust"
29+
echo ""
30+
fi
31+
32+
# Exit with a bad exit code if any dependencies are missing
33+
if [ "$missing_deps" -ne 0 ]; then
34+
echo "Install the missing dependencies and ensure they are on your path. Then run this command again."
35+
# TODO: remove sleep when cli bug is fixed
36+
sleep 2
37+
exit 1
38+
fi

servlets/fetch-image/src/lib.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
mod pdk;
2+
3+
use std::collections::BTreeMap;
4+
5+
use base64::Engine;
6+
use extism_pdk::*;
7+
use json::Value;
8+
use pdk::types::{
9+
CallToolRequest, CallToolResult, Content, ContentType, ListToolsResult, ToolDescription,
10+
};
11+
use serde_json::json;
12+
13+
pub(crate) fn call(input: CallToolRequest) -> Result<CallToolResult, Error> {
14+
match input.params.name.as_str() {
15+
"fetch-image" => fetch_image(input),
16+
_ => Ok(CallToolResult {
17+
is_error: Some(true),
18+
content: vec![Content {
19+
annotations: None,
20+
text: Some(format!("Unknown tool: {}", input.params.name)),
21+
mime_type: None,
22+
r#type: ContentType::Text,
23+
data: None,
24+
}],
25+
}),
26+
}
27+
}
28+
29+
fn fetch_image(input: CallToolRequest) -> Result<CallToolResult, Error> {
30+
let args = input.params.arguments.unwrap_or_default();
31+
let opt_mime_type = args.get("mime-type");
32+
let mut mime_type = "image/png".to_string();
33+
if opt_mime_type.is_some() {
34+
if let Some(Value::String(mime)) = opt_mime_type {
35+
match mime.as_str() {
36+
"image/png" | "image/jpeg" | "image/gif" | "image/webp" | "image/svg+xml" => {
37+
mime_type = mime.clone();
38+
}
39+
_ => {
40+
return Ok(CallToolResult {
41+
is_error: Some(true),
42+
content: vec![Content {
43+
annotations: None,
44+
text: Some("Invalid mime type".into()),
45+
mime_type: None,
46+
r#type: ContentType::Text,
47+
data: None,
48+
}],
49+
});
50+
}
51+
}
52+
}
53+
}
54+
55+
if let Some(Value::String(url)) = args.get("url") {
56+
// Create HTTP request
57+
let mut req = HttpRequest {
58+
url: url.clone(),
59+
headers: BTreeMap::new(),
60+
method: Some("GET".to_string()),
61+
};
62+
63+
// Add a user agent header to be polite
64+
req.headers
65+
.insert("User-Agent".to_string(), "fetch-tool/1.0".to_string());
66+
67+
// Let's filter by content type, we only want images
68+
req.headers.insert("Accept".to_string(), mime_type.clone());
69+
70+
// Perform the request
71+
let res = http::request::<()>(&req, None)?;
72+
73+
// Convert response body to string
74+
let body = res.body();
75+
76+
let encoded_image = base64::engine::general_purpose::STANDARD.encode(body.as_slice());
77+
78+
Ok(CallToolResult {
79+
is_error: None,
80+
content: vec![Content {
81+
annotations: None,
82+
data: Some(encoded_image),
83+
mime_type: Some(mime_type),
84+
r#type: ContentType::Image,
85+
text: None,
86+
}],
87+
})
88+
} else {
89+
Ok(CallToolResult {
90+
is_error: Some(true),
91+
content: vec![Content {
92+
annotations: None,
93+
text: Some("Please provide a url".into()),
94+
mime_type: None,
95+
r#type: ContentType::Text,
96+
data: None,
97+
}],
98+
})
99+
}
100+
}
101+
102+
// Called by mcpx to understand how and why to use this tool
103+
pub(crate) fn describe() -> Result<ListToolsResult, Error> {
104+
Ok(ListToolsResult{
105+
tools: vec![
106+
ToolDescription {
107+
name: "fetch-image".into(),
108+
description: "Enables to read images URLs. Fetches the contents of a URL pointing to an image and returns its contents converted to base64".into(),
109+
input_schema: json!({
110+
"type": "object",
111+
"properties": {
112+
"url": {
113+
"type": "string",
114+
"description": "The URL of the image to fetch",
115+
},
116+
"mime-type": {
117+
"type": "string",
118+
"description": "The mime type to filter by, it must be of the form image/png, image/jpeg, etc. \
119+
If the URL ends with an image extension, it should match with the mime type \
120+
(e.g. if the URL ends with .png, the mime type should be image/png, if the URL ends with .jpg,\
121+
the mime type should be image/jpeg, if the URL ends with .gif, the mime type should be image/gif, etc.).
122+
If the mime-type is not provided it will default to image/png",
123+
},
124+
},
125+
})
126+
.as_object()
127+
.unwrap()
128+
.clone(),
129+
},
130+
131+
],
132+
})
133+
}

0 commit comments

Comments
 (0)