plugins/turul-mcp-skills/skills/resource-prompt-patterns/SKILL.md
This skill should be used when the user asks to "create a resource", "add a resource", "MCP resource", "McpResource", "mcp_resource macro", "#[derive(McpResource)]", "ResourceBuilder", "resource URI", "URI template", "ResourceContent", "dynamic resource", "resource!", "create a prompt", "add a prompt", "MCP prompt", "McpPrompt", "#[derive(McpPrompt)]", "PromptBuilder", "prompt arguments", "PromptMessage", "prompt template", "GetPromptResult", "prompt!", "resources/read", "prompts/get", or "resource vs prompt". Covers creating MCP resources (function macro, derive, resource!{}, builder) and MCP prompts (derive, prompt!{}, builder) in the Turul MCP Framework (Rust).
npx skillsauth add aussierobots/turul-mcp-framework resource-prompt-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Resources and prompts are two of the three core MCP primitives (alongside tools). Resources expose data for clients to read; prompts expose conversational templates with arguments.
What are you providing?
├─ Data that clients READ (files, configs, API responses) ──→ Resource
└─ Conversational templates with ARGUMENTS ─────────────────→ Prompt
The framework provides four approaches to creating MCP resources:
Need a resource?
├─ Definitions known at compile time? ───→ Use macros
│ ├─ Need session context or complex read logic? ──→ Derive (#[derive(McpResource)]) + manual impl
│ ├─ Inline one-off with closure body? ────────────→ Declarative (resource!{})
│ └─ Otherwise ────────────────────────────────────→ Function Macro (#[mcp_resource]) ← DEFAULT
└─ Resources loaded from config/DB at runtime? ──────→ Builder (ResourceBuilder)
#[mcp_resource] (Start Here)Best for: Most resources. Typed parameters, auto-generated McpResource impl.
// turul-mcp-server v0.3
use turul_mcp_derive::mcp_resource;
use turul_mcp_server::prelude::*;
#[mcp_resource(
uri = "file:///logs/{service}.log",
name = "service_log",
description = "Recent log entries for a service",
mime_type = "text/plain"
)]
async fn service_log(service: String) -> McpResult<Vec<ResourceContent>> {
let content = read_log_file(&service).await?;
Ok(vec![ResourceContent::text(
&format!("file:///logs/{service}.log"),
content,
)])
}
let server = McpServer::builder()
.resource_fn(service_log) // Note: .resource_fn() for function macros
.build()?;
Key points:
{service}) become function parameters automaticallyuri is required; name, description, mime_type are optional.resource_fn() (NOT .resource())session: Option<&SessionContext> as second parameterSee: references/resource-guide.md for full attribute reference.
#[derive(McpResource)]Best for: Resources needing session access or complex read logic with a named struct.
// turul-mcp-server v0.3
use turul_mcp_derive::McpResource;
use turul_mcp_server::prelude::*;
#[derive(McpResource, Clone)]
#[resource(name = "profile", uri = "file:///users/{id}.json", description = "User profile")]
struct ProfileResource;
#[async_trait]
impl McpResource for ProfileResource {
async fn read(&self, params: Option<Value>, session: Option<&SessionContext>)
-> McpResult<Vec<ResourceContent>> {
let id = params.as_ref()
.and_then(|p| p.get("template_variables"))
.and_then(|tv| tv.get("id"))
.and_then(|v| v.as_str())
.ok_or(McpError::invalid_params("Missing id"))?;
Ok(vec![ResourceContent::text(
&format!("file:///users/{id}.json"),
get_profile(id).await?,
)])
}
}
let server = McpServer::builder()
.resource(ProfileResource) // Note: .resource() for derive
.build()?;
Key points:
McpResource::read() manuallyname, uri, description are required struct-level attributes; mime_type is optional#[content]/#[content_type] field attributes are accepted syntactically but NOT used for code generation — always implement read() explicitlyread() implresource!{}Best for: Inline one-off resources with closure body. Generates full struct + all traits + McpResource impl.
// turul-mcp-server v0.3
use turul_mcp_server::prelude::*;
let config = resource! {
uri: "file:///config.json",
name: "config",
description: "Application configuration",
content: |_params, _session| async move {
Ok(vec![ResourceContent::text("file:///config.json", load_config().await)])
}
};
let server = McpServer::builder()
.resource(config) // .resource() for declarative macro
.build()?;
Key points:
content: takes |params: Option<Value>, session: Option<&SessionContext>| async { ... }uri, name, description, content) are required.resource()ResourceBuilderBest for: Resources whose definitions are unknown at compile time (loaded from config/DB).
// turul-mcp-server v0.3
use turul_mcp_server::prelude::*;
let resource = ResourceBuilder::new("file:///status.json")
.description("System status")
.mime_type("application/json")
.read_text(|uri| async move {
Ok(get_status_json())
})
.build()?;
let server = McpServer::builder()
.resource(resource)
.build()?;
Key points:
.text_content() / .json_content() / .blob_content() for static content.read() / .read_text() for dynamic callbacks (receive URI string).name().resource()ResourceContent::text(uri, text) // Plain text
ResourceContent::blob(uri, base64_data, mime) // Binary (base64-encoded)
Multiple content items can be returned from a single read() call.
The framework provides three approaches to creating MCP prompts:
Need a prompt?
├─ Definition known at compile time? ───→ Use macros
│ ├─ Need custom struct with fields? ──→ Derive (#[derive(McpPrompt)]) + manual render()
│ └─ Inline one-off with closure? ─────→ Declarative (prompt!{})
└─ Prompts loaded at runtime? ───────────→ Builder (PromptBuilder)
Note: There is no #[mcp_prompt] function macro (unlike tools and resources).
#[derive(McpPrompt)]Best for: Prompts with typed fields that become MCP arguments.
// turul-mcp-server v0.3
use turul_mcp_derive::McpPrompt;
use turul_mcp_server::prelude::*;
#[derive(McpPrompt)]
#[prompt(name = "code_review", description = "Review code for issues")]
struct CodeReviewPrompt {
#[argument(description = "Programming language")]
language: String,
#[argument(description = "Code to review")]
code: String,
}
#[async_trait]
impl McpPrompt for CodeReviewPrompt {
async fn render(&self, args: Option<HashMap<String, Value>>)
-> McpResult<Vec<PromptMessage>> {
let lang = args.as_ref()
.and_then(|a| a.get("language"))
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let code = args.as_ref()
.and_then(|a| a.get("code"))
.and_then(|v| v.as_str())
.unwrap_or("");
Ok(vec![PromptMessage::user_text(
format!("Review this {lang} code for bugs and improvements:\n\n```{lang}\n{code}\n```")
)])
}
}
let server = McpServer::builder()
.prompt(CodeReviewPrompt { language: String::new(), code: String::new() })
.build()?;
Key points:
#[argument(description = "...")] on fields generates PromptArgument entriesMcpPrompt::render() manuallyrender() signature: (&self, args: Option<HashMap<String, Value>>) -> McpResult<Vec<PromptMessage>> — no session parameterOnceLock or capture external stateprompt!{}Best for: Inline one-off prompts with closure body. Generates full struct + all traits + McpPrompt impl.
// turul-mcp-server v0.3
use turul_mcp_server::prelude::*;
let greet = prompt! {
name: "greet",
description: "Greeting prompt",
arguments: {
name: String => "Person to greet", required
},
template: |args| async move {
let name = args.as_ref()
.and_then(|a| a.get("name"))
.and_then(|v| v.as_str())
.unwrap_or("World");
Ok(vec![PromptMessage::user_text(format!("Hello, {name}!"))])
}
};
let server = McpServer::builder()
.prompt(greet)
.build()?;
Key points:
arguments: block syntax: name: Type => "description" with optional required flagtemplate: takes |args: Option<HashMap<String, Value>>| async { ... }McpPrompt — no manual implementationname and template are required; description, title, arguments are optionalPromptBuilderBest for: Runtime-defined prompts with built-in template processing.
// turul-mcp-server v0.3
use turul_mcp_server::prelude::*;
let prompt = PromptBuilder::new("summarize")
.description("Summarize text")
.string_argument("text", "Text to summarize")
.optional_string_argument("style", "Summary style")
.template_user_message("Summarize in {style} style:\n\n{text}")
.build()?;
let server = McpServer::builder()
.prompt(prompt)
.build()?;
Key points:
{arg_name} placeholders auto-replaced in template messages.string_argument() creates required arg; .optional_string_argument() creates optional.get(callback) for fully dynamic generation (receives HashMap<String, String>, returns GetPromptResult).user_message() / .assistant_message() for static messages; .template_user_message() / .template_assistant_message() for templatesPromptMessage::user_text("content") // User role
PromptMessage::assistant_text("content") // Assistant role
PromptMessage::user_image(data, mime) // User image (base64)
Important: MCP has only User and Assistant roles — there is no System role.
| Feature | Resource fn macro | Resource derive | resource!{} | Resource builder | Prompt derive | prompt!{} | Prompt builder |
|---|---|---|---|---|---|---|---|
| Compile-time | Yes | Yes | Yes | No | Yes | Yes | No |
| Trait auto-gen | Full | Metadata only | Full | N/A | Metadata only | Full | N/A |
| Session access | Yes | Yes (manual) | Yes (closure) | No | No | No | No |
| Registration | .resource_fn() | .resource() | .resource() | .resource() | .prompt() | .prompt() | .prompt() |
| Best for | Default | Session + complex | Inline one-off | Runtime-defined | Struct w/ fields | Inline one-off | Runtime-defined |
ResourceBuilder when #[mcp_resource] suffices — builder is for runtime-defined resources only#[mcp_prompt] function macro to exist — prompts have derive, prompt!{}, and builder, but no function macro#[derive(McpResource)] to auto-generate read() — derive generates metadata traits only; implement read() manually (use resource!{} if you want auto-generation)#[derive(McpPrompt)] — must implement McpPrompt::render() manually (use prompt!{} if you want auto-generation).resource_fn() vs .resource() — function macros use _fn variant; all other patterns use .resource() / .prompt()Role::System in PromptMessage — MCP spec has only User and AssistantMcpPrompt::render() — render() takes args only, no session parameter; use OnceLock for session-dependent logic#[content]/#[content_type] as functional on derive — these are accepted syntactically but do not generate runtime content; always implement read() explicitlyTool creation? → See the tool-creation-patterns skill.
Output schemas, schemars, structuredContent? → See the output-schemas skill.
Client-side resource/prompt calls? → See the mcp-client-patterns skill.
Middleware (auth, rate limiting, logging)? → See the middleware-patterns skill for McpMiddleware, RequestContext, SessionInjection, and MiddlewareError.
Error handling (McpError variants, decision tree)? → See the error-handling-patterns skill for all 22 variants, error codes, and From conversions.
Session state? Use session.get_typed_state(key).await / session.set_typed_state(key, value).await?. See: CLAUDE.md — API Conventions
tools
This skill should be used when the user asks to "create a tool", "add a tool", "new tool", "which tool pattern", "compare tool patterns", "function macro vs derive", "mcp_tool macro", "#[mcp_tool]", "derive McpTool", "#[derive(McpTool)]", "ToolBuilder", "tool creation", "function macro tool", "server icon", "server branding", ".icons()", "Icon::data_uri", "server identity", "dynamic tools", "ToolChangeMode", "activate_tool", "deactivate_tool", "ToolRegistry", "tool_change_mode", or "notifications/tools/list_changed". Covers choosing between function macro (#[mcp_tool]), derive macro (#[derive(McpTool)]), and runtime builder (ToolBuilder) patterns, plus server identity (icons), in the Turul MCP Framework (Rust).
tools
This skill should be used when the user asks about "testing", "test patterns", "write tests", "unit test", "e2e test", "integration test", "McpTestClient", "TestServerManager", "compliance test", "test server", "test fixture", "doctest", "cargo test", "test organization", "SSE testing", or "test consolidation". Covers unit testing, E2E testing, compliance testing, SSE testing, and test organization in the Turul MCP Framework (Rust). McpTestClient is the in-process test harness; for the production client API see mcp-client-patterns.
tools
This skill should be used when the user asks about "task support", "TaskRuntime", "TaskStorage", "task_support attribute", "long-running tool", "CancellationHandle", "tasks/get", "tasks/list", "tasks/cancel", "tasks/result", "TaskStatus", "TaskRecord", "TaskOutcome", "InMemoryTaskStorage", "with_task_storage", "task state machine", "TaskExecutor", "TokioTaskExecutor", "task_support = optional", "task_support = required", "task_support = forbidden", "with_task_runtime", or "task storage backend". Covers MCP task support for long-running tools, state machine, storage backends, cancellation, and capability truthfulness in the Turul MCP Framework (Rust). TaskStorage is distinct from SessionStorage; for session persistence see session-storage-backends.
tools
This skill should be used when the user asks about "session storage", "SessionStorage trait", "SqliteSessionStorage", "PostgresSessionStorage", "DynamoDbSessionStorage", "InMemorySessionStorage", "session backend", "session persistence", "session events", "SSE reconnection storage", "which storage backend", "session TTL", "session cleanup", "session event management", "SseEvent", or "SessionStorageError". Covers the SessionStorage trait, backend selection, event management for SSE resumability, error types, and background cleanup in the Turul MCP Framework (Rust). Do NOT use for TaskStorage — task persistence is a separate trait; see task-patterns.