Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/mcpserver/apps-ui/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "apps-ui"
version = "0.1.0"
edition = "2021"

[dependencies]
poem-mcpserver.workspace = true
serde = { version = "1.0.219", features = ["derive"] }
schemars = "1.0"
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync"] }
23 changes: 23 additions & 0 deletions examples/mcpserver/apps-ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# MCP Apps UI Example

This example demonstrates MCP Apps using `poem-mcpserver` with a static UI resource.
The UI is bundled as an HTML file and uses `@modelcontextprotocol/ext-apps`.

## What it does

- Exposes a tool with `_meta.ui.resourceUri` (`ui://apps/color-picker`).
- Serves the UI resource via `resources/read`.
- Lets the UI call back into the server to set/get a color.

## Run the server

From the repository root:

- `cargo run -p apps-ui`

Then connect your MCP host to the server over stdio and invoke `color_app`.
The host should render the UI and allow interactive updates.

## Image

![](mcp_app.png)
Binary file added examples/mcpserver/apps-ui/mcp_app.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions examples/mcpserver/apps-ui/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use poem_mcpserver::{content::Text, stdio::stdio, tool::StructuredContent, McpServer, Tools};
use schemars::JsonSchema;
use serde::Serialize;

struct ColorTools {
color: String,
}

#[derive(Debug, Serialize, JsonSchema)]
struct ColorState {
color: String,
}

/// Minimal MCP Apps demo with a UI resource.
#[Tools]
impl ColorTools {
/// Open the color picker UI.
#[mcp(ui_resource = "ui://apps/color-picker")]
async fn color_app(&self) -> StructuredContent<ColorState> {
StructuredContent(ColorState {
color: self.color.clone(),
})
}

/// Get the current color.
async fn get_color(&self) -> StructuredContent<ColorState> {
StructuredContent(ColorState {
color: self.color.clone(),
})
}

/// Update the current color.
async fn set_color(&mut self, color: String) -> Text<String> {
self.color = color.clone();
Text(self.color.clone())
}
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
let server = McpServer::new()
.ui_resource(
"ui://apps/color-picker",
"Color Picker",
"Simple color picker UI powered by @modelcontextprotocol/ext-apps.",
"text/html",
include_str!("../ui/index.html"),
)
.tools(ColorTools {
color: "#ff0000".to_string(),
});

stdio(server).await
}
135 changes: 135 additions & 0 deletions examples/mcpserver/apps-ui/ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MCP Apps Color Picker</title>
<style>
body {
font-family: system-ui, -apple-system, Segoe UI, sans-serif;
margin: 0;
padding: 16px;
color: #111827;
background: #f8fafc;
}
.card {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
}
.row {
display: flex;
gap: 12px;
align-items: center;
margin-top: 12px;
}
.preview {
width: 48px;
height: 48px;
border-radius: 12px;
border: 1px solid #e2e8f0;
background: #ff0000;
}
button {
background: #2563eb;
color: white;
border: none;
border-radius: 8px;
padding: 8px 12px;
cursor: pointer;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.status {
font-size: 12px;
color: #64748b;
margin-top: 8px;
}
</style>
</head>
<body>
<div class="card">
<h2>Color Picker MCP App</h2>
<p>Select a color and sync it back to the MCP server.</p>
<div class="row">
<input id="color" type="color" value="#ff0000" />
<div id="preview" class="preview"></div>
<button id="save">Save</button>
</div>
<div id="status" class="status">Connecting...</div>
</div>

<script type="module">
import { App } from "https://esm.sh/@modelcontextprotocol/ext-apps";

const colorInput = document.getElementById("color");
const preview = document.getElementById("preview");
const status = document.getElementById("status");
const saveButton = document.getElementById("save");

const app = new App();

const applyColor = (color) => {
if (!color) return;
colorInput.value = color;
preview.style.background = color;
};

const updateFromResponse = (response) => {
const structured = response?.structuredContent;
if (structured?.color) {
applyColor(structured.color);
return;
}
const text = response?.content?.[0]?.text;
if (text) {
applyColor(text);
}
};

app.ontoolresult = (result) => {
const color = result?.structuredContent?.color || result?.data?.color;
applyColor(color);
};

const connect = async () => {
status.textContent = "Connecting to host...";
await app.connect();
status.textContent = "Connected. Loading current color...";
const response = await app.callServerTool({
name: "get_color",
arguments: {},
});
updateFromResponse(response);
status.textContent = "Ready.";
};

saveButton.addEventListener("click", async () => {
saveButton.disabled = true;
status.textContent = "Saving color...";
const response = await app.callServerTool({
name: "set_color",
arguments: { color: colorInput.value },
});
updateFromResponse(response);
await app.updateModelContext({
content: [
{
type: "text",
text: `User selected color ${colorInput.value}`,
},
],
});
status.textContent = "Saved.";
saveButton.disabled = false;
});

connect().catch((error) => {
status.textContent = `Failed to connect: ${error?.message ?? error}`;
});
</script>
</body>
</html>
14 changes: 14 additions & 0 deletions poem-mcpserver-macros/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub(crate) struct ToolsArgs {}
#[derive(FromMeta, Default)]
pub(crate) struct ToolArgs {
name: Option<String>,
#[darling(default)]
ui_resource: Option<String>,
}

#[derive(FromMeta, Default)]
Expand Down Expand Up @@ -89,6 +91,17 @@ pub(crate) fn generate(_args: ToolsArgs, mut item_impl: ItemImpl) -> Result<Toke
}
syn::ReturnType::Type(_, ty) => ty,
};
let tool_meta = match &tool_args.ui_resource {
Some(uri) => quote! {
::std::option::Option::Some(#crate_name::protocol::tool::ToolMeta {
ui: ::std::option::Option::Some(#crate_name::protocol::tool::ToolUi {
resource_uri: #uri.to_string(),
}),
})
},
None => quote!(::std::option::Option::None),
};

tools_descriptions.push(quote! {
#crate_name::protocol::tool::Tool {
name: #tool_name,
Expand All @@ -100,6 +113,7 @@ pub(crate) fn generate(_args: ToolsArgs, mut item_impl: ItemImpl) -> Result<Toke
output_schema: std::option::Option::map(<#resp_ty as #crate_name::tool::IntoToolResponse>::output_schema(), |schema| {
#crate_name::private::serde_json::to_value(schema).expect("serialize output schema")
}),
meta: #tool_meta,
},
});

Expand Down
34 changes: 33 additions & 1 deletion poem-mcpserver/src/protocol/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct ResourcesListRequest {
}

/// Resource information.
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Resource {
/// The uri of the resource.
Expand All @@ -24,10 +24,42 @@ pub struct Resource {
pub mime_type: String,
}

/// A request to read resources.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesReadRequest {
/// The uri of the resource.
pub uri: String,
}

/// Resource content.
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ResourceContent {
/// The uri of the resource.
pub uri: String,
/// The mime type of the resource.
pub mime_type: String,
/// Text content, if any.
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
/// Base64-encoded binary content, if any.
#[serde(skip_serializing_if = "Option::is_none")]
pub blob: Option<String>,
}

/// A response to a resources/list request.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesListResponse {
/// Resources list.
pub resources: Vec<Resource>,
}

/// A response to a resources/read request.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesReadResponse {
/// Resources contents.
pub contents: Vec<ResourceContent>,
}
11 changes: 9 additions & 2 deletions poem-mcpserver/src/protocol/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use serde_json::Value;
use crate::protocol::{
initialize::InitializeRequest,
prompts::{PromptsGetRequest, PromptsListRequest},
resources::{ResourcesListRequest, ResourcesReadRequest},
tool::{ToolsCallRequest, ToolsListRequest},
};

Expand Down Expand Up @@ -71,9 +72,15 @@ pub enum Requests {
/// Resources list.
#[serde(rename = "resources/list")]
ResourcesList {
/// Prompts list request parameters.
/// Resources list request parameters.
#[serde(default)]
params: PromptsListRequest,
params: ResourcesListRequest,
},
/// Read a resource.
#[serde(rename = "resources/read")]
ResourcesRead {
/// Resources read request parameters.
params: ResourcesReadRequest,
},
}

Expand Down
20 changes: 20 additions & 0 deletions poem-mcpserver/src/protocol/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ pub struct Tool {
/// The output schema of the tool, if any.
#[serde(skip_serializing_if = "Option::is_none")]
pub output_schema: Option<Value>,
/// The tool metadata.
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<ToolMeta>,
}

/// Tool metadata.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolMeta {
/// UI metadata.
#[serde(skip_serializing_if = "Option::is_none")]
pub ui: Option<ToolUi>,
}

/// Tool UI metadata.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolUi {
/// UI resource URI.
pub resource_uri: String,
}

/// A response to a tools/list request.
Expand Down
Loading