use std::net::SocketAddr;
use std::sync::Arc;
pub mod stateless;
async fn yield_to_runtime() {
tokio::task::yield_now().await;
}
use protocol_droid::application::server::Server;
use protocol_droid::application::server::context::RequestContext;
use protocol_droid::domain::ir::{
AudioContent, Content, EmbeddedResource, ImageContent, JsonObject, LogLevel, PromptMessage,
ResourceContents, Role, TextContent, ToolResult,
};
use protocol_droid::domain::registry::{Prompt, PromptArgument, Resource, Tool};
use protocol_droid::serde::Serialize;
use protocol_droid::serde_json;
const PNG_1X1: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
const WAV_SILENT: &str = "UklGRiQAAABXQVZFZm10IBAAAAABAAEAgD4AAAB9AAACABAAZGF0YQAAAAA=";
const SERVER_NAME: &str = "conformance-reference-server";
const ELICITATION_MESSAGE: &str = "This tool requires user input.";
#[derive(Serialize)]
struct EchoResult {
text: String,
length: usize,
}
#[derive(Serialize)]
struct AddResult {
total: i64,
}
struct ConformanceFixture;
#[protocol_droid::server(name = "conformance-reference-server")]
impl ConformanceFixture {
#[protocol_droid::tool]
async fn echo(text: String) -> Result<EchoResult, String> {
yield_to_runtime().await;
Ok(EchoResult {
length: text.chars().count(),
text: text.to_uppercase(),
})
}
#[protocol_droid::tool]
async fn add(a: i64, b: i64) -> Result<AddResult, String> {
yield_to_runtime().await;
Ok(AddResult { total: a + b })
}
#[protocol_droid::tool]
async fn test_simple_text() -> ToolResult {
yield_to_runtime().await;
ToolResult::text("This is a simple text response.")
}
#[protocol_droid::tool]
async fn test_image_content() -> ToolResult {
yield_to_runtime().await;
ToolResult::new(vec![Content::Image(ImageContent::new(
PNG_1X1,
"image/png",
))])
}
#[protocol_droid::tool]
async fn test_audio_content() -> ToolResult {
yield_to_runtime().await;
ToolResult::new(vec![Content::Audio(AudioContent::new(
WAV_SILENT,
"audio/wav",
))])
}
#[protocol_droid::tool]
async fn test_embedded_resource() -> ToolResult {
yield_to_runtime().await;
let mut embedded = EmbeddedResource::text(
"test://embedded-resource",
"This is an embedded resource's text.",
);
embedded.mime_type = Some("text/plain".to_owned());
ToolResult::new(vec![Content::EmbeddedResource(embedded)])
}
#[protocol_droid::tool]
async fn test_tool_with_logging(ctx: RequestContext) -> ToolResult {
yield_to_runtime().await;
for message in [
"Tool execution started",
"Tool processing data",
"Tool execution completed",
] {
log_message(&ctx, message).await;
}
ToolResult::text("Logged three messages.")
}
#[protocol_droid::tool]
async fn test_tool_with_progress(ctx: RequestContext) -> ToolResult {
for (index, progress) in [0, 50, 100].into_iter().enumerate() {
if index > 0 {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
progress_message(&ctx, progress, 100).await;
}
ToolResult::text("Reported progress.")
}
#[protocol_droid::tool]
async fn test_sampling(ctx: RequestContext, prompt: String) -> ToolResult {
yield_to_runtime().await;
let Some(request) = sampling_request(&prompt) else {
return ToolResult::text("The sampling request could not be built.");
};
match ctx.sample(&request).await {
Ok(_) => ToolResult::text("Sampling completed."),
Err(failure) => ToolResult::text(format!("Sampling was not completed: {failure}")),
}
}
#[protocol_droid::tool]
async fn test_elicitation(ctx: RequestContext, message: String) -> ToolResult {
yield_to_runtime().await;
let Some(request) = elicitation_request(&message) else {
return ToolResult::text("The elicitation request could not be built.");
};
match ctx.elicit(&request).await {
Ok(_) => ToolResult::text("Elicitation completed."),
Err(failure) => ToolResult::text(format!("Elicitation was not completed: {failure}")),
}
}
#[protocol_droid::tool(name = "test_elicitation_sep1034_defaults")]
async fn test_elicitation_sep1034_defaults(ctx: RequestContext) -> ToolResult {
yield_to_runtime().await;
let Some(request) = elicitation_with_schema(SEP1034_SCHEMA) else {
return ToolResult::text("The elicitation request could not be built.");
};
match ctx.elicit(&request).await {
Ok(answer) => elicitation_report(&answer),
Err(failure) => ToolResult::text(format!("Elicitation was not completed: {failure}")),
}
}
#[protocol_droid::tool(name = "test_elicitation_sep1330_enums")]
async fn test_elicitation_sep1330_enums(ctx: RequestContext) -> ToolResult {
yield_to_runtime().await;
let Some(request) = elicitation_with_schema(SEP1330_SCHEMA) else {
return ToolResult::text("The elicitation request could not be built.");
};
match ctx.elicit(&request).await {
Ok(answer) => elicitation_report(&answer),
Err(failure) => ToolResult::text(format!("Elicitation was not completed: {failure}")),
}
}
#[protocol_droid::tool]
async fn test_error_handling() -> ToolResult {
yield_to_runtime().await;
ToolResult::text("This tool always returns an error.").error()
}
#[protocol_droid::tool]
async fn test_multiple_content_types() -> ToolResult {
yield_to_runtime().await;
let mut embedded = EmbeddedResource::text(
"test://embedded-resource",
"This is an embedded resource's text.",
);
embedded.mime_type = Some("text/plain".to_owned());
ToolResult::new(vec![
Content::Text(protocol_droid::domain::ir::TextContent::new(
"This is a multi-type response.",
)),
Content::Image(ImageContent::new(PNG_1X1, "image/png")),
Content::EmbeddedResource(embedded),
])
}
#[protocol_droid::prompt(getter = get_test_prompt_with_embedded_resource)]
fn test_prompt_with_embedded_resource() -> Prompt {
Prompt {
description: Some("A prompt whose message embeds a resource.".to_owned()),
arguments: vec![PromptArgument {
name: "resourceUri".to_owned(),
description: Some("The URI of the resource to embed.".to_owned()),
title: None,
required: true,
}],
..Prompt::new("test_prompt_with_embedded_resource")
}
}
#[protocol_droid::prompt(getter = get_test_prompt_with_image)]
fn test_prompt_with_image() -> Prompt {
Prompt {
description: Some("A prompt whose message is an image.".to_owned()),
..Prompt::new("test_prompt_with_image")
}
}
#[protocol_droid::prompt(getter = get_review)]
fn review() -> Prompt {
Prompt {
description: Some(
"Asks for a review of a diff against an optional focus area.".to_owned(),
),
arguments: vec![
PromptArgument {
name: "diff".to_owned(),
description: Some("The diff to review.".to_owned()),
title: None,
required: true,
},
PromptArgument {
name: "focus".to_owned(),
description: Some(
"What to look at (e.g. `security`, `performance`).".to_owned(),
),
title: None,
required: false,
},
],
..Prompt::new("review")
}
}
#[protocol_droid::prompt(getter = get_summarize)]
fn summarize() -> Prompt {
Prompt {
description: Some("Summarizes whatever the caller hands in.".to_owned()),
..Prompt::new("summarize")
}
}
#[protocol_droid::prompt(getter = get_test_simple_prompt)]
fn test_simple_prompt() -> Prompt {
Prompt {
description: Some("A simple prompt with no arguments.".to_owned()),
..Prompt::new("test_simple_prompt")
}
}
#[protocol_droid::prompt(getter = get_test_prompt_with_arguments)]
fn test_prompt_with_arguments() -> Prompt {
Prompt {
description: Some("A prompt that substitutes two arguments.".to_owned()),
arguments: vec![
PromptArgument {
name: "arg1".to_owned(),
description: Some("First test argument.".to_owned()),
title: None,
required: true,
},
PromptArgument {
name: "arg2".to_owned(),
description: Some("Second test argument.".to_owned()),
title: None,
required: true,
},
],
..Prompt::new("test_prompt_with_arguments")
}
}
#[protocol_droid::resource(reader = read_readme)]
fn readme() -> Resource {
Resource {
description: Some("The reference server's own readme.".to_owned()),
mime_type: Some("text/markdown".to_owned()),
..Resource::new("file:///conformance/readme.md", "readme")
}
}
#[protocol_droid::resource(reader = read_changelog)]
fn changelog() -> Resource {
Resource {
description: Some("The reference server's own changelog.".to_owned()),
mime_type: Some("text/markdown".to_owned()),
..Resource::new("file:///conformance/CHANGELOG.md", "changelog")
}
}
#[protocol_droid::resource(reader = read_static_text)]
fn static_text_resource() -> Resource {
Resource {
description: Some("A static text resource for the read scenario.".to_owned()),
mime_type: Some("text/plain".to_owned()),
..Resource::new("test://static-text", "static_text")
}
}
#[protocol_droid::resource(reader = read_static_binary)]
fn static_binary_resource() -> Resource {
Resource {
description: Some("A static binary resource for the read scenario.".to_owned()),
mime_type: Some("image/png".to_owned()),
..Resource::new("test://static-binary", "static_binary")
}
}
#[protocol_droid::resource(reader = read_example_resource)]
fn example_resource() -> Resource {
Resource {
description: Some("The resource the embedded-resource prompt embeds.".to_owned()),
mime_type: Some("text/plain".to_owned()),
..Resource::new("test://example-resource", "example_resource")
}
}
#[protocol_droid::resource(reader = read_watched_resource)]
fn watched_resource() -> Resource {
Resource {
description: Some("A resource the suite subscribes to.".to_owned()),
mime_type: Some("text/plain".to_owned()),
..Resource::new("test://watched-resource", "watched_resource")
}
}
#[protocol_droid::resource(reader = read_template_data)]
fn template_data() -> Resource {
Resource {
description: Some("The resource a template URI resolves to.".to_owned()),
mime_type: Some("text/plain".to_owned()),
..Resource::new("test://template/123/data", "template_data")
}
}
fn read_readme(_context: RequestContext, uri: &str) -> Vec<ResourceContents> {
vec![static_text(
uri,
"# Reference server\n\nA fixture for the conformance suite.\n",
)]
}
fn read_changelog(_context: RequestContext, uri: &str) -> Vec<ResourceContents> {
vec![static_text(
uri,
"# Changelog\n\n## 0.1.0\n\n- First release.\n",
)]
}
fn read_static_text(_context: RequestContext, uri: &str) -> Vec<ResourceContents> {
vec![static_text(
uri,
"This is the content of the static text resource.",
)]
}
fn read_static_binary(_context: RequestContext, uri: &str) -> Vec<ResourceContents> {
vec![ResourceContents::blob(uri, PNG_1X1).with_mime_type("image/png")]
}
fn read_example_resource(_context: RequestContext, uri: &str) -> Vec<ResourceContents> {
vec![static_text(uri, "This is the example resource's text.")]
}
fn read_watched_resource(_context: RequestContext, uri: &str) -> Vec<ResourceContents> {
vec![static_text(uri, "This resource is watched by the client.")]
}
fn read_template_data(_context: RequestContext, uri: &str) -> Vec<ResourceContents> {
let id = uri
.split('/')
.nth(3)
.filter(|segment| !segment.is_empty())
.unwrap_or_default();
let body = format!(r#"{{"id":"{id}","templateTest":true,"data":"Data for ID: {id}"}}"#);
vec![ResourceContents::text(uri, &body).with_mime_type("application/json")]
}
fn get_test_prompt_with_embedded_resource(
_context: RequestContext,
arguments: &JsonObject,
) -> Vec<PromptMessage> {
let uri = string_argument(arguments, "resourceUri")
.unwrap_or_else(|| "test://example-resource".to_owned());
vec![PromptMessage::new(
Role::User,
Content::EmbeddedResource(EmbeddedResource::text(
uri,
"This is an embedded resource's text.",
)),
)]
}
fn get_test_prompt_with_image(
_context: RequestContext,
_arguments: &JsonObject,
) -> Vec<PromptMessage> {
vec![PromptMessage::new(
Role::User,
Content::Image(ImageContent::new(PNG_1X1, "image/png")),
)]
}
fn get_review(_context: RequestContext, arguments: &JsonObject) -> Vec<PromptMessage> {
let focus = string_argument(arguments, "focus");
vec![PromptMessage::new(
Role::User,
Content::Text(TextContent::new(format!(
"Review this diff{}.",
focus.map_or_else(String::new, |focus| format!(", focusing on {focus}"))
))),
)]
}
fn get_summarize(_context: RequestContext, _arguments: &JsonObject) -> Vec<PromptMessage> {
vec![PromptMessage::new(
Role::User,
Content::Text(TextContent::new("Summarize the following.")),
)]
}
fn get_test_simple_prompt(
_context: RequestContext,
_arguments: &JsonObject,
) -> Vec<PromptMessage> {
vec![PromptMessage::new(
Role::User,
Content::Text(TextContent::new("This is a simple prompt for testing.")),
)]
}
fn get_test_prompt_with_arguments(
_context: RequestContext,
arguments: &JsonObject,
) -> Vec<PromptMessage> {
let arg1 = string_argument(arguments, "arg1").unwrap_or_else(|| "<missing>".to_owned());
let arg2 = string_argument(arguments, "arg2").unwrap_or_else(|| "<missing>".to_owned());
vec![PromptMessage::new(
Role::User,
Content::Text(TextContent::new(format!(
"Prompt with arguments: arg1='{arg1}', arg2='{arg2}'"
))),
)]
}
}
fn sampling_request(prompt: &str) -> Option<JsonObject> {
serde_json::to_string(prompt)
.ok()
.and_then(|text| {
JsonObject::new(format!(
r#"{{"messages":[{{"role":"user","content":{{"type":"text","text":{text}}}}}],"maxTokens":16}}"#
))
.ok()
})
}
fn elicitation_request(message: &str) -> Option<JsonObject> {
serde_json::to_string(message).ok().and_then(|text| {
JsonObject::new(format!(
r#"{{"message":{text},"requestedSchema":{{"type":"object","properties":{{"answer":{{"type":"string","title":"Answer"}}}},"required":["answer"]}}}}"#
))
.ok()
})
}
const SEP1034_SCHEMA: &str = r#"{
"type": "object",
"properties": {
"name": { "type": "string", "title": "Name", "default": "John Doe" },
"age": { "type": "integer", "title": "Age", "default": 30 },
"score": { "type": "number", "title": "Score", "default": 95.5 },
"status": {
"type": "string",
"title": "Status",
"enum": ["active", "inactive", "pending"],
"default": "active"
},
"verified": { "type": "boolean", "title": "Verified", "default": true }
},
"required": ["name", "age", "score", "status", "verified"]
}"#;
const SEP1330_SCHEMA: &str = r#"{
"type": "object",
"properties": {
"untitledSingle": {
"type": "string",
"enum": ["option1", "option2", "option3"]
},
"titledSingle": {
"type": "string",
"oneOf": [
{ "const": "value1", "title": "First Option" },
{ "const": "value2", "title": "Second Option" }
]
},
"legacyEnum": {
"type": "string",
"enum": ["opt1", "opt2", "opt3"],
"enumNames": ["Option One", "Option Two", "Option Three"]
},
"untitledMulti": {
"type": "array",
"items": { "type": "string", "enum": ["option1", "option2", "option3"] }
},
"titledMulti": {
"type": "array",
"items": {
"anyOf": [
{ "const": "value1", "title": "First Choice" },
{ "const": "value2", "title": "Second Choice" }
]
}
}
},
"required": ["untitledSingle", "titledSingle", "legacyEnum", "untitledMulti", "titledMulti"]
}"#;
const JSON_SCHEMA_2020_12_INPUT: &str = r##"{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"$defs": {
"address": {
"$anchor": "addressDef",
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" }
}
}
},
"properties": {
"name": { "type": "string" },
"address": { "$ref": "#/$defs/address" },
"contactMethod": { "type": "string", "enum": ["phone", "email"] },
"phone": { "type": "string" },
"email": { "type": "string" }
},
"allOf": [
{
"anyOf": [
{ "required": ["phone"] },
{ "required": ["email"] }
]
}
],
"if": {
"properties": { "contactMethod": { "const": "phone" } },
"required": ["contactMethod"]
},
"then": { "required": ["phone"] },
"else": { "required": ["email"] },
"additionalProperties": false
}"##;
fn json_schema_2020_12_result(arguments: &JsonObject) -> JsonObject {
let echoed = format!(r#"{{"received":{}}}"#, arguments.as_str());
JsonObject::new(echoed).unwrap_or_default()
}
fn elicitation_with_schema(schema: &str) -> Option<JsonObject> {
serde_json::to_string(ELICITATION_MESSAGE)
.ok()
.and_then(|text| {
JsonObject::new(format!(
r#"{{"message":{text},"requestedSchema":{schema}}}"#
))
.ok()
})
}
fn elicitation_report(answer: &JsonObject) -> ToolResult {
let parsed: serde_json::Value = match serde_json::from_str(answer.as_str()) {
Ok(parsed) => parsed,
Err(failure) => {
return ToolResult::text(format!(
"Elicitation answered with unreadable JSON: {failure}"
));
}
};
let action = parsed.get("action").and_then(serde_json::Value::as_str);
let content = parsed.get("content").unwrap_or(&serde_json::Value::Null);
action.map_or_else(
|| ToolResult::text(format!("Elicitation answered with no action: {content}")),
|action| {
ToolResult::text(format!(
"Elicitation completed: action={action}, content={content}"
))
},
)
}
async fn log_message(ctx: &RequestContext, message: &str) {
let _outcome = ctx.log(LogLevel::Info, message).await;
}
async fn progress_message(ctx: &RequestContext, progress: i64, total: i64) {
let Ok(params) = JsonObject::new(format!(r#"{{"progress":{progress},"total":{total}}}"#))
else {
return;
};
let _outcome = ctx.report_progress(¶ms).await;
}
fn static_text(uri: &str, body: &str) -> ResourceContents {
ResourceContents::text(uri, body).with_mime_type("text/markdown")
}
fn string_argument(arguments: &JsonObject, key: &str) -> Option<String> {
let parsed: serde_json::Value = serde_json::from_str(arguments.as_str()).ok()?;
parsed
.get(key)
.and_then(serde_json::Value::as_str)
.map(str::to_owned)
}
fn parse_addr() -> Result<SocketAddr, Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
if arg == "--addr"
&& let Some(value) = args.next()
{
return Ok(value.parse()?);
}
}
Ok("127.0.0.1:8080".parse()?)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(serve())
}
async fn serve() -> Result<(), Box<dyn std::error::Error>> {
let addr = parse_addr()?;
let mut server: Server = ConformanceFixture.into_server()?;
stateless::register(&mut server).await?;
server.add_tool(
Tool::new("json_schema_2020_12_tool", JSON_SCHEMA_2020_12_INPUT)?
.described("Tool with JSON Schema 2020-12 features"),
Arc::new(|_ctx: RequestContext, arguments: JsonObject| {
Box::pin(async move { json_schema_2020_12_result(&arguments) })
}),
);
let config = protocol_droid::adapters::primary::transport::http::ServerConfig::new()?
.with_streaming(protocol_droid::adapters::primary::transport::http::Streaming::EventStream);
let builder = protocol_droid::adapters::primary::transport::http::HttpBuilder::new(server)
.addr(addr)
.config(config);
#[cfg(feature = "legacy-sse")]
let builder = if std::env::args().any(|arg| arg == "--legacy-sse") {
builder.legacy_sse()
} else {
builder
};
let bound = builder.serve().await?;
eprintln!("{}: listening on {}", SERVER_NAME, bound.addr);
Ok(())
}