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
2 changes: 1 addition & 1 deletion Cargo.lock

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

66 changes: 50 additions & 16 deletions e2e/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ fn test_mcp_search_patents() {
}

#[test]
fn test_mcp_fetch_patent() {
fn test_mcp_claims_query_patterns() {
let mut child = Command::new(env!("CARGO_BIN_EXE_google-patent-cli"))
.arg("mcp")
.stdin(Stdio::piped())
Expand All @@ -211,37 +211,71 @@ fn test_mcp_fetch_patent() {
"clientInfo": { "name": "test", "version": "1.0" }
}
});
writeln!(stdin, "{}", init_request).expect("Failed to write init");
writeln!(stdin, "{}", init_request).unwrap();
let mut response = String::new();
reader.read_line(&mut response).expect("Failed to read init response");
reader.read_line(&mut response).unwrap();

// 2. Initialized notification
let initialized_notification = json!({
"jsonrpc": "2.0",
"method": "notifications/initialized"
});
writeln!(stdin, "{}", initialized_notification).expect("Failed to write initialized");
let notif = json!({"jsonrpc": "2.0", "method": "notifications/initialized"});
writeln!(stdin, "{}", notif).unwrap();

// 3. Call fetch_patent
// 3. Fetch a patent
let fetch_request = json!({
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "fetch_patent",
"arguments": { "patent_id": "US9152718B2" }
}
});
writeln!(stdin, "{}", fetch_request).unwrap();
response.clear();
reader.read_line(&mut response).unwrap();
assert!(response.contains("output_file"), "Fetch failed: {}", response);

let fetch_result: serde_json::Value = serde_json::from_str(&response).unwrap();
let dataset = fetch_result["result"]["content"][0]["text"].as_str().unwrap();
let dataset_json: serde_json::Value = serde_json::from_str(dataset).unwrap();
let dataset_name = dataset_json["dataset"].as_str().unwrap().to_string();

// 4. Query claims via label - c.text should return claim body
let req = json!({
"jsonrpc": "2.0",
"id": 10,
"method": "tools/call",
"params": {
"name": "execute_cypher",
"arguments": {
"patent_id": "US9152718B2"
"dataset": dataset_name,
"query": "MATCH (c:claims) RETURN c.text LIMIT 1"
}
}
});
writeln!(stdin, "{}", fetch_request).expect("Failed to write fetch call");

writeln!(stdin, "{}", req).unwrap();
response.clear();
reader.read_line(&mut response).expect("Failed to read fetch response");
reader.read_line(&mut response).unwrap();
assert!(response.contains("c.text"), "c.text not in response: {}", response);
assert!(!response.contains("\"c.text\": \"null\""), "c.text is null");

// Now returns summary with output_file and schema
assert!(response.contains("output_file"), "Fetch response missing output_file: {}", response);
assert!(response.contains("schema"), "Fetch response missing schema: {}", response);
// 5. Query claims via relationship - should also work
let req = json!({
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "execute_cypher",
"arguments": {
"dataset": dataset_name,
"query": "MATCH (p:Patent)-[:claims]->(c:claims) RETURN c.number, c.text LIMIT 1"
}
}
});
writeln!(stdin, "{}", req).unwrap();
response.clear();
reader.read_line(&mut response).unwrap();
assert!(response.contains("c.text"), "c.text not in relationship query: {}", response);
assert!(response.contains("c.number"), "c.number not in relationship query: {}", response);

// Re-attach stdin for graceful_shutdown
child.stdin = Some(stdin);
Expand Down
69 changes: 68 additions & 1 deletion src/mcp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ pub async fn run() -> anyhow::Result<()> {
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::core::models::{Patent, SearchResult};
use crate::core::models::{Claim, Patent, SearchResult};

struct MockSearcher;

Expand Down Expand Up @@ -601,4 +601,71 @@ mod tests {
let err = result.unwrap_err();
assert!(err.message.contains("Fetch failed"));
}

#[tokio::test]
async fn test_claims_via_cypher_relationship() {
let patent_with_claims = Patent {
id: "US123".to_string(),
title: "Test Patent".to_string(),
abstract_text: Some("Test abstract".to_string()),
claims: Some(vec![
Claim {
number: "1".to_string(),
id: "c1".to_string(),
text: "A method for doing X.".to_string(),
},
Claim {
number: "2".to_string(),
id: "c2".to_string(),
text: "The method of claim 1, further comprising Y.".to_string(),
},
]),
..Default::default()
};

let json_value = serde_json::to_value(&patent_with_claims).unwrap();
let engine = CypherEngine::from_json_with_label(&json_value, "Patent").unwrap();

// Verify claims are accessible via label directly
let result = engine.execute("MATCH (c:claims) RETURN c.text").unwrap();
let json = result.as_json_array();
let arr = json.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["c.text"], "A method for doing X.");
assert_eq!(arr[1]["c.text"], "The method of claim 1, further comprising Y.");

// Verify claims are accessible via relationship
let result = engine
.execute("MATCH (p:Patent)-[:claims]->(c:claims) RETURN c.number, c.text")
.unwrap();
let json = result.as_json_array();
let arr = json.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["c.text"], "A method for doing X.");

// Verify p.claims is null
let result = engine.execute("MATCH (p:Patent) RETURN p.claims").unwrap();
let json = result.as_json_array();
let arr = json.as_array().unwrap();
assert_eq!(arr[0]["p.claims"], "null");
}

#[test]
fn test_parse_text_property() {
let engine = CypherEngine::from_json_with_label(
&serde_json::json!({"id": "1", "text": "hello"}),
"Test",
)
.unwrap();

// c.text should parse and execute correctly
let result = engine.execute("MATCH (c:Test) RETURN c.text");
assert!(result.is_ok(), "c.text parse/execute failed: {:?}", result.err());
let json = result.unwrap().as_json_array();
assert_eq!(json[0]["c.text"], "hello");

// c.id should also work
let result = engine.execute("MATCH (c:Test) RETURN c.id");
assert!(result.is_ok(), "c.id parse/execute failed: {:?}", result.err());
}
}
Loading