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
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
"Bash(gh release list:*)",
"Bash(gh release view:*)",
"Bash(gh pr list:*)",
"Bash(pre-commit:*)"
"Bash(pre-commit:*)",
"WebFetch(domain:spec.modelcontextprotocol.io)"
],
"deny": []
}
Expand Down
70 changes: 70 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,76 @@ All notable changes to the PulseEngine MCP Framework will be documented in this
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.13.0] - 2025-01-11

### Fixed

#### mcp-macros

- **[BREAKING] Fixed `#[mcp_tools]` macro to use JSON serialization instead of Rust Debug format** ([#62](https://github.com/pulseengine/mcp/issues/62))
- Tool return values are now properly serialized as JSON using `serde_json::to_string()`
- Populates `structured_content` field in `CallToolResult` per MCP 2025-06-18 specification
- Graceful fallback to Debug format for types that don't implement `Serialize`
- **Breaking Change**: Tool return types should implement `Serialize` trait for optimal JSON output
- Previously returned Rust Debug format like `SearchResult { items: [...] }` which broke JSON parsing
- Now returns proper JSON: `{"items": [...]}`

### Added

#### Testing

- **Comprehensive JSON serialization test suite** (`mcp-macros/tests/json_serialization_test.rs`)
- Tests for structured return types (nested structs, vectors, enums)
- Tests for Result<T, E> return types
- Tests for simple types (string, number, bool, vector)
- Verification that `structured_content` field is populated
- Verification that Debug format markers are not present in output
- 8 comprehensive test cases covering all scenarios

### Changed

- **Version bumped to 0.13.0** (breaking change due to Serialize requirement)
- Tool responses now comply with MCP 2025-06-18 specification for structured content

### Migration Guide

If you have tools that return structured types:

```rust
// Add Serialize to your return types
#[derive(Debug, Serialize)] // Add Serialize
struct MyResult {
data: Vec<String>,
}

#[mcp_tools]
impl MyServer {
pub fn my_tool(&self) -> MyResult {
MyResult { data: vec!["item1".to_string()] }
}
}
```

For types that can't implement Serialize, the macro will gracefully fall back to Debug format.

## [0.12.0] - 2025-01-11

### Added

- **MCP 2025-06-18 protocol support**
- `NumberOrString` type for request IDs
- Optional `_meta` fields across protocol types

### Fixed

- **Fixed flaky tests** with `serial_test` crate
- All environment variable tests now run serially to prevent race conditions
- Added `#[serial_test::serial]` to tests in mcp-security-middleware, mcp-cli-derive, and mcp-cli

### Changed

- CI now validates all changes with pre-commit hooks

## [0.4.1] - 2024-07-06

### Added
Expand Down
28 changes: 14 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 13 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ members = [
resolver = "2"

[workspace.package]
version = "0.12.0"
version = "0.13.0"
rust-version = "1.88"
edition = "2024"
license = "MIT OR Apache-2.0"
Expand Down Expand Up @@ -108,18 +108,18 @@ assert_matches = "1.5"
serde_yaml = "0.9"

# Framework internal dependencies (published versions)
pulseengine-mcp-protocol = { version = "0.12.0", path = "mcp-protocol" }
pulseengine-mcp-logging = { version = "0.12.0", path = "mcp-logging" }
pulseengine-mcp-auth = { version = "0.12.0", path = "mcp-auth" }
pulseengine-mcp-security = { version = "0.12.0", path = "mcp-security" }
pulseengine-mcp-security-middleware = { version = "0.12.0", path = "mcp-security-middleware" }
pulseengine-mcp-monitoring = { version = "0.12.0", path = "mcp-monitoring" }
pulseengine-mcp-transport = { version = "0.12.0", path = "mcp-transport" }
pulseengine-mcp-cli = { version = "0.12.0", path = "mcp-cli" }
pulseengine-mcp-cli-derive = { version = "0.12.0", path = "mcp-cli-derive" }
pulseengine-mcp-server = { version = "0.12.0", path = "mcp-server" }
pulseengine-mcp-macros = { version = "0.12.0", path = "mcp-macros" }
pulseengine-mcp-external-validation = { version = "0.12.0", path = "mcp-external-validation" }
pulseengine-mcp-protocol = { version = "0.13.0", path = "mcp-protocol" }
pulseengine-mcp-logging = { version = "0.13.0", path = "mcp-logging" }
pulseengine-mcp-auth = { version = "0.13.0", path = "mcp-auth" }
pulseengine-mcp-security = { version = "0.13.0", path = "mcp-security" }
pulseengine-mcp-security-middleware = { version = "0.13.0", path = "mcp-security-middleware" }
pulseengine-mcp-monitoring = { version = "0.13.0", path = "mcp-monitoring" }
pulseengine-mcp-transport = { version = "0.13.0", path = "mcp-transport" }
pulseengine-mcp-cli = { version = "0.13.0", path = "mcp-cli" }
pulseengine-mcp-cli-derive = { version = "0.13.0", path = "mcp-cli-derive" }
pulseengine-mcp-server = { version = "0.13.0", path = "mcp-server" }
pulseengine-mcp-macros = { version = "0.13.0", path = "mcp-macros" }
pulseengine-mcp-external-validation = { version = "0.13.0", path = "mcp-external-validation" }

[profile.release]
opt-level = "s"
Expand Down
59 changes: 46 additions & 13 deletions mcp-macros/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,29 +158,62 @@ pub fn generate_error_handling(return_type: &syn::ReturnType) -> TokenStream {
if let Some(segment) = type_path.path.segments.last() {
if segment.ident == "Result" {
// It's already a Result, wrap it properly for the dispatch context
// Use JSON serialization for structured data, with fallback
return quote! {
match result {
Ok(value) => Ok(pulseengine_mcp_protocol::CallToolResult {
content: vec![pulseengine_mcp_protocol::Content::text(format!("{:?}", value))],
is_error: Some(false),
structured_content: None,
_meta: None,
}),
Ok(value) => {
// Try JSON serialization first (for structured data)
let (text_content, structured) = match serde_json::to_value(&value) {
Ok(json_value) => {
// Serialize as JSON string for text content
let text = serde_json::to_string(&value)
.unwrap_or_else(|_| format!("{:?}", value));
(text, Some(json_value))
}
Err(_) => {
// Fallback to Debug if not serializable
(format!("{:?}", value), None)
}
};

Ok(pulseengine_mcp_protocol::CallToolResult {
content: vec![pulseengine_mcp_protocol::Content::text(text_content)],
is_error: Some(false),
structured_content: structured,
_meta: None,
})
}
Err(e) => Err(pulseengine_mcp_protocol::Error::internal_error(e.to_string())),
}
};
}
}
}

// Not a Result, wrap it with simple Display formatting
// Not a Result, wrap it with JSON serialization (preferred) or Display formatting
quote! {
Ok(pulseengine_mcp_protocol::CallToolResult {
content: vec![pulseengine_mcp_protocol::Content::text(result.to_string())],
is_error: Some(false),
structured_content: None,
_meta: None,
})
{
// Try JSON serialization first (for structured data)
let (text_content, structured) = match serde_json::to_value(&result) {
Ok(json_value) => {
// Serialize as JSON string for text content
let text = serde_json::to_string(&result)
.unwrap_or_else(|_| format!("{:?}", result));
(text, Some(json_value))
}
Err(_) => {
// Fallback to Debug if not serializable
(format!("{:?}", result), None)
}
};

Ok(pulseengine_mcp_protocol::CallToolResult {
content: vec![pulseengine_mcp_protocol::Content::text(text_content)],
is_error: Some(false),
structured_content: structured,
_meta: None,
})
}
}
}
}
Expand Down
Loading