Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2c46028

Browse files
authoredApr 5, 2025··
chore: house keeping, more tests, improved documentation (#10)
* chore: house keeping * add more tests * test: add more macro tests
1 parent 05f4729 commit 2c46028

File tree

6 files changed

+524
-2
lines changed

6 files changed

+524
-2
lines changed
 

‎CONTRIBUTING.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# **Contributing to rust-mcp-sdk**
2+
3+
🎉 Thank you for your interest in improving **rust-mcp-sdk**! Every contribution, big or small, is valuable and appreciated.
4+
5+
## **Code of Conduct**
6+
7+
We follow the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct). Please be respectful and inclusive when contributing.
8+
9+
## **How to Contribute**
10+
11+
### Participating in Tests, Documentation, and Examples
12+
13+
We highly encourage contributors to improve test coverage, enhance documentation, and introduce new examples to ensure the reliability and usability of the project. If you notice untested code paths, missing documentation, or areas where examples could help, consider adding tests, clarifying explanations, or providing real-world usage examples. Every improvement helps make the project more robust, well-documented, and accessible to others!
14+
15+
### Participating in Issues
16+
17+
You can contribute in three key ways:
18+
19+
1. **Report Issues** – If you find a bug or have an idea, open an issue for discussion.
20+
2. **Help Triage** – Provide details, test cases, or suggestions to clarify issues.
21+
3. **Resolve Issues** – Investigate problems and submit fixes via Pull Requests (PRs).
22+
23+
Anyone can participate at any stage, whether it's discussing, triaging, or reviewing PRs.
24+
25+
### **Filing a Bug Report**
26+
27+
When reporting a bug, use the provided issue template and fill in as many details as possible. Don’t worry if you can’t answer everything—just provide what you can.
28+
29+
### **Fixing Issues**
30+
31+
Most issues are resolved through a Pull Request. PRs go through a review process to ensure quality and correctness.
32+
33+
## **Pull Requests (PRs)**
34+
35+
We welcome PRs! Before submitting, please:
36+
37+
1. **Discuss major changes** – Open an issue before adding a new feature and opening a PR.
38+
2. **Create a feature branch** – Fork the repo and branch from `main`.
39+
3. **Write tests** – If your change affects functionality, add relevant tests.
40+
4. **Update documentation** – If you modify APIs, update the docs.
41+
5. **Run tests** – Make sure all tests succeed by running:
42+
43+
```sh
44+
cargo make test
45+
```
46+
47+
### **Commit Best Practices**
48+
49+
- **Relate PR changes to the issue** – Changes in a pull request (PR) should directly address the specific issue it’s tied to. Unrelated changes should be split into separate issues and PRs to maintain focus and simplify review.
50+
- **Logically separate commits** – Keep changes atomic and easy to review.
51+
- **Maintain a bisect-able history** – Each commit should compile and pass all tests to enable easy debugging with `git bisect` in case of regression.
52+
53+
## License
54+
55+
By contributing to rust-mcp-sdk, you acknowledge and agree that your contributions will be licensed under the terms specified in the LICENSE file located in the root directory of this repository.
56+
57+
---

‎README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,21 @@ The same principles outlined above apply to the client-side handlers, `mcp_clien
213213
Use `client_runtime::create_client()` or `client_runtime_core::create_client()` , respectively.
214214
Check out the corresponding examples at: [examples/simple-mcp-client](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client) and [examples/simple-mcp-client-core](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/simple-mcp-client-core).
215215

216+
## Contributing
217+
218+
We welcome everyone who wishes to contribute! Please refer to the [contributing](CONTRIBUTING.md) guidelines for more details.
219+
220+
Check out our [development guide](development.md) for instructions on setting up, building, testing, formatting, and trying out example projects.
221+
222+
All contributions, including issues and pull requests, must follow
223+
Rust's Code of Conduct.
224+
225+
Unless explicitly stated otherwise, any contribution you submit for inclusion in rust-mcp-sdk is provided under the terms of the MIT License, without any additional conditions or restrictions.
226+
227+
## Development
228+
229+
Check out our [development guide](development.md) for instructions on setting up, building, testing, formatting, and trying out example projects.
230+
216231
## License
217232

218233
This project is licensed under the MIT License. see the [LICENSE](LICENSE) file for details.

‎crates/rust-mcp-macros/src/lib.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,60 @@ pub fn derive_json_schema(input: TokenStream) -> TokenStream {
296296
};
297297
TokenStream::from(expanded)
298298
}
299+
300+
#[cfg(test)]
301+
mod tests {
302+
use super::*;
303+
use syn::parse_str;
304+
#[test]
305+
fn test_valid_macro_attributes() {
306+
let input = r#"name = "test_tool", description = "A test tool.""#;
307+
let parsed: MCPToolMacroAttributes = parse_str(input).unwrap();
308+
309+
assert_eq!(parsed.name.unwrap(), "test_tool");
310+
assert_eq!(parsed.description.unwrap(), "A test tool.");
311+
}
312+
313+
#[test]
314+
fn test_missing_name() {
315+
let input = r#"description = "Only description""#;
316+
let result: Result<MCPToolMacroAttributes, Error> = parse_str(input);
317+
assert!(result.is_err());
318+
assert_eq!(
319+
result.err().unwrap().to_string(),
320+
"The 'name' attribute is required."
321+
)
322+
}
323+
324+
#[test]
325+
fn test_missing_description() {
326+
let input = r#"name = "OnlyName""#;
327+
let result: Result<MCPToolMacroAttributes, Error> = parse_str(input);
328+
assert!(result.is_err());
329+
assert_eq!(
330+
result.err().unwrap().to_string(),
331+
"The 'description' attribute is required."
332+
)
333+
}
334+
335+
#[test]
336+
fn test_empty_name_field() {
337+
let input = r#"name = "", description = "something""#;
338+
let result: Result<MCPToolMacroAttributes, Error> = parse_str(input);
339+
assert!(result.is_err());
340+
assert_eq!(
341+
result.err().unwrap().to_string(),
342+
"The 'name' attribute should not be an empty string."
343+
);
344+
}
345+
#[test]
346+
fn test_empty_description_field() {
347+
let input = r#"name = "my-tool", description = """#;
348+
let result: Result<MCPToolMacroAttributes, Error> = parse_str(input);
349+
assert!(result.is_err());
350+
assert_eq!(
351+
result.err().unwrap().to_string(),
352+
"The 'description' attribute should not be an empty string."
353+
);
354+
}
355+
}

‎crates/rust-mcp-macros/src/utils.rs

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,326 @@ pub fn renamed_field(attrs: &[Attribute]) -> Option<String> {
236236

237237
renamed
238238
}
239+
240+
#[cfg(test)]
241+
mod tests {
242+
use super::*;
243+
use quote::quote;
244+
use syn::parse_quote;
245+
246+
fn render(ts: proc_macro2::TokenStream) -> String {
247+
ts.to_string().replace(char::is_whitespace, "")
248+
}
249+
250+
#[test]
251+
fn test_is_option() {
252+
let ty: Type = parse_quote!(Option<String>);
253+
assert!(is_option(&ty));
254+
255+
let ty: Type = parse_quote!(Vec<String>);
256+
assert!(!is_option(&ty));
257+
}
258+
259+
#[test]
260+
fn test_is_vec() {
261+
let ty: Type = parse_quote!(Vec<i32>);
262+
assert!(is_vec(&ty));
263+
264+
let ty: Type = parse_quote!(Option<i32>);
265+
assert!(!is_vec(&ty));
266+
}
267+
268+
#[test]
269+
fn test_get_inner_type() {
270+
let ty: Type = parse_quote!(Option<String>);
271+
let inner = get_inner_type(&ty);
272+
assert!(inner.is_some());
273+
let inner = inner.unwrap();
274+
assert_eq!(quote!(#inner).to_string(), quote!(String).to_string());
275+
276+
let ty: Type = parse_quote!(Vec<i32>);
277+
let inner = get_inner_type(&ty);
278+
assert!(inner.is_some());
279+
let inner = inner.unwrap();
280+
assert_eq!(quote!(#inner).to_string(), quote!(i32).to_string());
281+
282+
let ty: Type = parse_quote!(i32);
283+
assert!(get_inner_type(&ty).is_none());
284+
}
285+
286+
#[test]
287+
fn test_might_be_struct() {
288+
let ty: Type = parse_quote!(MyStruct);
289+
assert!(might_be_struct(&ty));
290+
291+
let ty: Type = parse_quote!(String);
292+
assert!(!might_be_struct(&ty));
293+
}
294+
295+
#[test]
296+
fn test_type_to_json_schema_string() {
297+
let ty: Type = parse_quote!(String);
298+
let attrs: Vec<Attribute> = vec![];
299+
let tokens = type_to_json_schema(&ty, &attrs);
300+
let output = tokens.to_string();
301+
assert!(output.contains("\"string\""));
302+
}
303+
304+
#[test]
305+
fn test_type_to_json_schema_option() {
306+
let ty: Type = parse_quote!(Option<i32>);
307+
let attrs: Vec<Attribute> = vec![];
308+
let tokens = type_to_json_schema(&ty, &attrs);
309+
let output = tokens.to_string();
310+
assert!(output.contains("\"nullable\""));
311+
}
312+
313+
#[test]
314+
fn test_type_to_json_schema_vec() {
315+
let ty: Type = parse_quote!(Vec<String>);
316+
let attrs: Vec<Attribute> = vec![];
317+
let tokens = type_to_json_schema(&ty, &attrs);
318+
let output = tokens.to_string();
319+
assert!(output.contains("\"array\""));
320+
}
321+
322+
#[test]
323+
fn test_has_derive() {
324+
let attr: Attribute = parse_quote!(#[derive(Clone, Debug)]);
325+
assert!(has_derive(&[attr.clone()], "Debug"));
326+
assert!(!has_derive(&[attr], "Serialize"));
327+
}
328+
329+
#[test]
330+
fn test_renamed_field() {
331+
let attr: Attribute = parse_quote!(#[serde(rename = "renamed")]);
332+
assert_eq!(renamed_field(&[attr]), Some("renamed".to_string()));
333+
334+
let attr: Attribute = parse_quote!(#[serde(skip_serializing_if = "Option::is_none")]);
335+
assert_eq!(renamed_field(&[attr]), None);
336+
}
337+
338+
#[test]
339+
fn test_get_doc_comment_single_line() {
340+
let attrs: Vec<Attribute> = vec![parse_quote!(#[doc = "This is a test comment."])];
341+
let result = super::get_doc_comment(&attrs);
342+
assert_eq!(result, Some("This is a test comment.".to_string()));
343+
}
344+
345+
#[test]
346+
fn test_get_doc_comment_multi_line() {
347+
let attrs: Vec<Attribute> = vec![
348+
parse_quote!(#[doc = "Line one."]),
349+
parse_quote!(#[doc = "Line two."]),
350+
parse_quote!(#[doc = "Line three."]),
351+
];
352+
let result = super::get_doc_comment(&attrs);
353+
assert_eq!(
354+
result,
355+
Some("Line one.\nLine two.\nLine three.".to_string())
356+
);
357+
}
358+
359+
#[test]
360+
fn test_get_doc_comment_no_doc() {
361+
let attrs: Vec<Attribute> = vec![parse_quote!(#[allow(dead_code)])];
362+
let result = super::get_doc_comment(&attrs);
363+
assert_eq!(result, None);
364+
}
365+
366+
#[test]
367+
fn test_get_doc_comment_trim_whitespace() {
368+
let attrs: Vec<Attribute> = vec![parse_quote!(#[doc = " Trimmed line. "])];
369+
let result = super::get_doc_comment(&attrs);
370+
assert_eq!(result, Some("Trimmed line.".to_string()));
371+
}
372+
373+
#[test]
374+
fn test_renamed_field_basic() {
375+
let attrs = vec![parse_quote!(#[serde(rename = "new_name")])];
376+
let result = renamed_field(&attrs);
377+
assert_eq!(result, Some("new_name".to_string()));
378+
}
379+
380+
#[test]
381+
fn test_renamed_field_without_rename() {
382+
let attrs = vec![parse_quote!(#[serde(default)])];
383+
let result = renamed_field(&attrs);
384+
assert_eq!(result, None);
385+
}
386+
387+
#[test]
388+
fn test_renamed_field_with_multiple_attrs() {
389+
let attrs = vec![
390+
parse_quote!(#[serde(default)]),
391+
parse_quote!(#[serde(rename = "actual_name")]),
392+
];
393+
let result = renamed_field(&attrs);
394+
assert_eq!(result, Some("actual_name".to_string()));
395+
}
396+
397+
#[test]
398+
fn test_renamed_field_irrelevant_attribute() {
399+
let attrs = vec![parse_quote!(#[some_other_attr(value = "irrelevant")])];
400+
let result = renamed_field(&attrs);
401+
assert_eq!(result, None);
402+
}
403+
404+
#[test]
405+
fn test_renamed_field_ignores_other_serde_keys() {
406+
let attrs = vec![parse_quote!(#[serde(skip_serializing_if = "Option::is_none")])];
407+
let result = renamed_field(&attrs);
408+
assert_eq!(result, None);
409+
}
410+
411+
#[test]
412+
fn test_has_derive_positive() {
413+
let attrs: Vec<Attribute> = vec![parse_quote!(#[derive(Debug, Clone)])];
414+
assert!(has_derive(&attrs, "Debug"));
415+
assert!(has_derive(&attrs, "Clone"));
416+
}
417+
418+
#[test]
419+
fn test_has_derive_negative() {
420+
let attrs: Vec<Attribute> = vec![parse_quote!(#[derive(Serialize, Deserialize)])];
421+
assert!(!has_derive(&attrs, "Debug"));
422+
}
423+
424+
#[test]
425+
fn test_has_derive_no_derive_attr() {
426+
let attrs: Vec<Attribute> = vec![parse_quote!(#[allow(dead_code)])];
427+
assert!(!has_derive(&attrs, "Debug"));
428+
}
429+
430+
#[test]
431+
fn test_has_derive_multiple_attrs() {
432+
let attrs: Vec<Attribute> = vec![
433+
parse_quote!(#[allow(unused)]),
434+
parse_quote!(#[derive(PartialEq)]),
435+
parse_quote!(#[derive(Eq)]),
436+
];
437+
assert!(has_derive(&attrs, "PartialEq"));
438+
assert!(has_derive(&attrs, "Eq"));
439+
assert!(!has_derive(&attrs, "Clone"));
440+
}
441+
442+
#[test]
443+
fn test_has_derive_empty_attrs() {
444+
let attrs: Vec<Attribute> = vec![];
445+
assert!(!has_derive(&attrs, "Debug"));
446+
}
447+
448+
#[test]
449+
fn test_might_be_struct_with_custom_type() {
450+
let ty: syn::Type = parse_quote!(MyStruct);
451+
assert!(might_be_struct(&ty));
452+
}
453+
454+
#[test]
455+
fn test_might_be_struct_with_primitive_type() {
456+
let primitives = [
457+
"i32", "u64", "bool", "f32", "String", "Option", "Vec", "char", "str",
458+
];
459+
for ty_str in &primitives {
460+
let ty: syn::Type = syn::parse_str(ty_str).unwrap();
461+
assert!(
462+
!might_be_struct(&ty),
463+
"Expected '{}' to be not a struct",
464+
ty_str
465+
);
466+
}
467+
}
468+
469+
#[test]
470+
fn test_might_be_struct_with_namespaced_type() {
471+
let ty: syn::Type = parse_quote!(std::collections::HashMap<String, i32>);
472+
assert!(!might_be_struct(&ty)); // segments.len() > 1
473+
}
474+
475+
#[test]
476+
fn test_might_be_struct_with_generic_arguments() {
477+
let ty: syn::Type = parse_quote!(MyStruct<T>);
478+
assert!(!might_be_struct(&ty)); // has type arguments
479+
}
480+
481+
#[test]
482+
fn test_might_be_struct_with_empty_type_path() {
483+
let ty: syn::Type = parse_quote!(());
484+
assert!(!might_be_struct(&ty));
485+
}
486+
487+
#[test]
488+
fn test_json_schema_string() {
489+
let ty: syn::Type = parse_quote!(String);
490+
let tokens = type_to_json_schema(&ty, &[]);
491+
let output = render(tokens);
492+
assert!(output
493+
.contains("\"type\".to_string(),serde_json::Value::String(\"string\".to_string())"));
494+
}
495+
496+
#[test]
497+
fn test_json_schema_number() {
498+
let ty: syn::Type = parse_quote!(i32);
499+
let tokens = type_to_json_schema(&ty, &[]);
500+
let output = render(tokens);
501+
assert!(output
502+
.contains("\"type\".to_string(),serde_json::Value::String(\"number\".to_string())"));
503+
}
504+
505+
#[test]
506+
fn test_json_schema_boolean() {
507+
let ty: syn::Type = parse_quote!(bool);
508+
let tokens = type_to_json_schema(&ty, &[]);
509+
let output = render(tokens);
510+
assert!(output
511+
.contains("\"type\".to_string(),serde_json::Value::String(\"boolean\".to_string())"));
512+
}
513+
514+
#[test]
515+
fn test_json_schema_vec_of_string() {
516+
let ty: syn::Type = parse_quote!(Vec<String>);
517+
let tokens = type_to_json_schema(&ty, &[]);
518+
let output = render(tokens);
519+
assert!(output
520+
.contains("\"type\".to_string(),serde_json::Value::String(\"array\".to_string())"));
521+
assert!(output.contains("\"items\".to_string(),serde_json::Value::Object"));
522+
}
523+
524+
#[test]
525+
fn test_json_schema_option_of_number() {
526+
let ty: syn::Type = parse_quote!(Option<u64>);
527+
let tokens = type_to_json_schema(&ty, &[]);
528+
let output = render(tokens);
529+
assert!(output.contains("\"nullable\".to_string(),serde_json::Value::Bool(true)"));
530+
assert!(output
531+
.contains("\"type\".to_string(),serde_json::Value::String(\"number\".to_string())"));
532+
}
533+
534+
#[test]
535+
fn test_json_schema_custom_struct() {
536+
let ty: syn::Type = parse_quote!(MyStruct);
537+
let tokens = type_to_json_schema(&ty, &[]);
538+
let output = render(tokens);
539+
assert!(output.contains("MyStruct::json_schema()"));
540+
}
541+
542+
#[test]
543+
fn test_json_schema_with_doc_comment() {
544+
let ty: syn::Type = parse_quote!(String);
545+
let attrs: Vec<Attribute> = vec![parse_quote!(#[doc = "A user name."])];
546+
let tokens = type_to_json_schema(&ty, &attrs);
547+
let output = render(tokens);
548+
assert!(output.contains(
549+
"\"description\".to_string(),serde_json::Value::String(\"Ausername.\".to_string())"
550+
));
551+
}
552+
553+
#[test]
554+
fn test_json_schema_fallback_unknown() {
555+
let ty: syn::Type = parse_quote!((i32, i32));
556+
let tokens = type_to_json_schema(&ty, &[]);
557+
let output = render(tokens);
558+
assert!(output
559+
.contains("\"type\".to_string(),serde_json::Value::String(\"unknown\".to_string())"));
560+
}
561+
}

‎crates/rust-mcp-macros/tests/macro_test.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ pub mod common;
77
fn test_rename() {
88
let schema = EditOperation::json_schema();
99

10-
println!(">>> schema {:?} ", schema);
11-
1210
assert_eq!(schema.len(), 3);
1311

1412
assert!(schema.contains_key("properties"));

‎development.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Development
2+
3+
This document outlines the process for compiling this crate's source code on your local machine.
4+
5+
## Prerequisites
6+
7+
Ensure you have the following installed:
8+
9+
- The latest stable version of **Rust**
10+
- [`cargo-nextest`](https://crates.io/crates/cargo-nextest) for running tests
11+
- [`cargo-make`](https://crates.io/crates/cargo-make/0.3.54) for running tasks like tests
12+
13+
## Setting Up the Development Environment
14+
15+
1- Clone the repository:
16+
17+
```sh
18+
git clone https://github.com/rust-mcp-stack/rust-mcp-sdk
19+
cd rust-mcp-sdk
20+
```
21+
22+
2- Install dependencies: The Rust project uses Cargo for dependency management. To install dependencies, run:
23+
24+
```sh
25+
cargo build
26+
```
27+
28+
## Running Examples
29+
30+
Example projects can be found in the [/examples](/examples) folder of the repository.
31+
Build and run instructions are available in their respective README.md files.
32+
33+
You can run examples by passing the example project name to Cargo using the `-p` argument, like this:
34+
35+
```sh
36+
cargo run -p simple-mcp-client
37+
```
38+
39+
You can build the examples in a similar way. The following command builds the project and generates the binary at `target/release/hello-world-mcp-server`:
40+
41+
```sh
42+
43+
cargo build -p hello-world-mcp-server --release
44+
```
45+
46+
## Code Formatting
47+
48+
We follow the default Rust formatting style enforced by `rustfmt`. To format your code, run:
49+
50+
```sh
51+
cargo fmt
52+
```
53+
54+
Additionally, we use **Clippy** for linting Rust code. You can check for linting issues by running:
55+
56+
```sh
57+
cargo make clippy
58+
```
59+
60+
Please ensure your code is formatted and free of Clippy warnings before submitting any changes.
61+
62+
## Testing
63+
64+
We use [`cargo-nextest`](https://crates.io/crates/cargo-nextest) to run our test suite.
65+
66+
### Running Tests
67+
68+
To run the tests, use:
69+
70+
```sh
71+
cargo make test
72+
```

0 commit comments

Comments
 (0)
Please sign in to comment.