MCP Response Patterns

Best practices for returning tool results with both human-readable and structured content.

MCP tools return CallToolResult which contains both human-readable content and machine-readable structured data. This document covers best practices for constructing responses.

CallToolResult Structure

pub struct CallToolResult {
    pub content: Vec<Content>,              // Human-readable output
    pub structured_content: Option<Value>,  // Machine-readable JSON
    pub is_error: Option<bool>,             // Success/failure flag
    pub meta: Option<Map<String, Value>>,   // Additional metadata
}

The content Field

The content field contains human-readable output displayed to users. It's a vector of Content items.

Text Content

Most common - plain text or markdown:

use rmcp::model::Content;

// Simple text
Content::text("Operation completed successfully.")

// Markdown formatting
Content::text(format!(
    "## Research Complete\n\n\
     **Topic:** {topic}\n\n\
     **Sources Found:** {count}\n\n\
     Use artifact ID `{artifact_id}` for the next step."
))

// Multi-paragraph
Content::text(format!(
    "Created blog post: {title}\n\n\
     Word count: {word_count}\n\n\
     The content has been saved. You can now generate social media posts."
))

Multiple Content Items

Return multiple content blocks:

Ok(CallToolResult {
    content: vec![
        Content::text("## Summary\n\nOperation completed."),
        Content::text(format!("### Details\n\n{details}")),
    ],
    // ...
})

Image Content

For tools that generate images:

Content::image(
    base64_encoded_data,
    "image/png",
)

The structured_content Field

Machine-readable JSON for programmatic consumption. Include data that clients might parse.

When to Include

Scenario Include structured_content?
Tool creates an artifact Yes - include artifact_id
Tool returns data clients parse Yes
Tool performs action with no data Optional
Error response Optional - can include error details

Basic Structure

use serde_json::json;

Ok(CallToolResult {
    content: vec![Content::text("...")],
    structured_content: Some(json!({
        "artifact_id": artifact_id.to_string(),
        "topic": topic,
        "source_count": sources.len(),
        "status": "completed"
    })),
    is_error: Some(false),
    meta: None,
})

Artifact Reference

When a tool creates an artifact:

structured_content: Some(json!({
    "artifact_id": artifact_id.to_string(),
    "topic": topic,
    "status": "completed",
    "next_step": "Call create_blog_post with this artifact_id"
}))

Rich Data

Include data that clients can display or process:

structured_content: Some(json!({
    "artifact_id": artifact_id.to_string(),
    "topic": topic,
    "source_count": sources.len(),
    "query_count": queries.len(),
    "research_summary": summary,
    "sources": sources.iter().map(|s| json!({
        "title": s.title,
        "uri": s.uri,
        "relevance": s.relevance
    })).collect::<Vec<_>>(),
    "status": "completed"
}))

Artifact Type Header

Use x-artifact-type for typed artifacts:

structured_content: Some(json!({
    "x-artifact-type": "blog_artifact",
    "skill_id": skill_id,
    "artifact": {
        "title": title,
        "slug": slug,
        "content": content,
        "word_count": word_count
    }
}))

The is_error Field

Always set explicitly:

// Success
is_error: Some(false)

// Error
is_error: Some(true)

The meta Field

Optional metadata for cross-references:

pub fn create_result_meta(artifact_id: &str) -> Map<String, Value> {
    let mut meta = serde_json::Map::new();
    meta.insert("artifact_id".to_string(), json!(artifact_id));
    meta.insert(
        "ui_uri".to_string(),
        json!(format!("ui://my-server/{artifact_id}"))
    );
    meta
}

// Usage
Ok(CallToolResult {
    content: vec![Content::text("...")],
    structured_content: Some(json!({...})),
    is_error: Some(false),
    meta: Some(create_result_meta(artifact_id.as_str())),
})

Success Response Patterns

Simple Success

Ok(CallToolResult {
    content: vec![Content::text("Task completed successfully.")],
    structured_content: Some(json!({"status": "completed"})),
    is_error: Some(false),
    meta: None,
})

With Artifact

Ok(CallToolResult {
    content: vec![Content::text(format!(
        "## Research Complete\n\n\
         **Topic:** {topic}\n\n\
         Found {source_count} sources.\n\n\
         **Artifact ID:** `{artifact_id}`\n\n\
         Use this artifact_id when calling create_blog_post."
    ))],
    structured_content: Some(json!({
        "artifact_id": artifact_id.to_string(),
        "topic": topic,
        "source_count": source_count,
        "status": "completed"
    })),
    is_error: Some(false),
    meta: Some(create_result_meta(artifact_id.as_str())),
})

With Rich Data

Ok(CallToolResult {
    content: vec![Content::text(format!(
        "## Blog Post Created\n\n\
         **Title:** {title}\n\n\
         **Slug:** {slug}\n\n\
         **Word Count:** {word_count}\n\n\
         Content ID: `{content_id}`"
    ))],
    structured_content: Some(json!({
        "x-artifact-type": "blog_artifact",
        "content_id": content_id.to_string(),
        "title": title,
        "slug": slug,
        "word_count": word_count,
        "skill_id": skill_id,
        "status": "completed"
    })),
    is_error: Some(false),
    meta: None,
})

Error Response Patterns

Parameter Error

return Err(McpError::invalid_params(
    "Missing required parameter: topic",
    None,
));

Validation Error

if skill_id != "research_blog" {
    return Err(McpError::invalid_params(
        format!("skill_id must be 'research_blog', got '{skill_id}'"),
        None,
    ));
}

Operation Error (Partial Result)

When an operation fails but there's useful context:

Ok(CallToolResult {
    content: vec![Content::text(format!(
        "## Error\n\n\
         Failed to complete research: {error}\n\n\
         Partial results may be available."
    ))],
    structured_content: Some(json!({
        "status": "error",
        "error": error.to_string(),
        "partial_results": partial_data
    })),
    is_error: Some(true),
    meta: None,
})

Command Failure

For CLI-style tools:

if !output.success {
    return Ok(CallToolResult {
        content: vec![Content::text(format!(
            "Command failed (exit code {}):\n\n```\n{}\n```",
            output.exit_code, output.stderr
        ))],
        structured_content: Some(json!({
            "status": "error",
            "exit_code": output.exit_code,
            "stderr": output.stderr
        })),
        is_error: Some(true),
        meta: None,
    });
}

Best Practices

1. Always Set is_error

// Don't
meta: None,
// Do
is_error: Some(false),  // or Some(true)
meta: None,

2. Include Next Steps

Guide the user on what to do next:

Content::text(format!(
    "Research complete. Artifact ID: `{artifact_id}`\n\n\
     **Next step:** Call `create_blog_post` with:\n\
     - `artifact_id`: `{artifact_id}`\n\
     - `skill_id`: `blog_writing` or `technical_content_writing`\n\
     - `slug`: your-post-slug\n\
     - `description`: SEO description\n\
     - `keywords`: [\"keyword1\", \"keyword2\"]\n\
     - `instructions`: specific writing instructions"
))

3. Format IDs Consistently

Use backticks for IDs in text:

// Good
format!("Artifact ID: `{artifact_id}`")

// Also good for emphasis
format!("**Artifact ID:** `{artifact_id}`")

4. Match Schema to Output

Your output should match output_schema():

// In helpers.rs
pub fn output_schema() -> Value {
    json!({
        "type": "object",
        "properties": {
            "artifact_id": {"type": "string"},
            "status": {"type": "string", "enum": ["completed", "error"]}
        },
        "required": ["artifact_id", "status"]
    })
}

// In handler.rs - ensure response matches
structured_content: Some(json!({
    "artifact_id": artifact_id.to_string(),  // Required
    "status": "completed"                     // Required, valid enum
}))

5. Log Completion

Log successful operations:

tracing::info!(
    topic = %topic,
    artifact_id = %artifact_id,
    source_count = %source_count,
    "Research completed"
);

Ok(CallToolResult { /* ... */ })

Quick Reference

Field Type Purpose
content Vec<Content> Human-readable output
structured_content Option<Value> Machine-readable JSON
is_error Option<bool> Success/failure flag
meta Option<Map> Artifact references, URIs
Content Type Use For
Content::text() Text/markdown output
Content::image() Image data (base64)
Error Type Use For
McpError::invalid_params() Bad parameter values
McpError::invalid_request() Malformed request
McpError::internal_error() Server-side failures
McpError::method_not_found() Unknown tool