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
1 change: 1 addition & 0 deletions crates/mcp-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
async-trait = "0.1"
tracing = "0.1"
94 changes: 79 additions & 15 deletions crates/mcp-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ use std::sync::Arc;
use thiserror::Error;
use tokio::sync::RwLock;

// ---------------------------------------------------------------------------
// Error types
// ---------------------------------------------------------------------------

#[derive(Error, Debug)]
pub enum McpError {
#[error("tool not found: {0}")]
Expand All @@ -19,6 +23,27 @@ pub enum McpError {
InvalidRequest(String),
}

impl McpError {
/// Returns a human-readable recovery hint for this error.
pub fn recovery_hint(&self) -> &'static str {
match self {
McpError::ToolNotFound(_) => {
"Verify the tool name is correct. Use list_tools() to see available tools."
}
McpError::ResourceNotFound(_) => {
"Verify the resource URI is correct. Use list_resources() to see available resources."
}
McpError::InvalidRequest(_) => {
"Check that the request payload matches the expected schema for this tool/resource."
}
}
}
Comment on lines +26 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add a clear AgilePlus trace (existing item ID or spec creation path) for this newly introduced behavior so the change is visibly linked to tracked work instead of appearing ad hoc. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The final file introduces a new recovery_hint API and accompanying behavior, but there is no AgilePlus spec reference or creation/tracking path anywhere in the changed code. This matches the custom rule requiring new feature work to be traced to an AgilePlus item.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/mcp-server/src/lib.rs
**Line:** 26:40
**Comment:**
	*Custom Rule: Add a clear AgilePlus trace (existing item ID or spec creation path) for this newly introduced behavior so the change is visibly linked to tracked work instead of appearing ad hoc.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}

// ---------------------------------------------------------------------------
// Core data types
// ---------------------------------------------------------------------------

/// Tool definition
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
Expand Down Expand Up @@ -49,18 +74,22 @@ pub struct Resource {
pub mime_type: Option<String>,
}

/// MCP Server
pub struct McpServer {
tools: Arc<RwLock<HashMap<String, ToolHandler>>>,
resources: Arc<RwLock<HashMap<String, Resource>>>,
}

#[derive(Clone)]
pub struct ToolHandler {
pub tool: Tool,
pub handler: Arc<dyn Fn(serde_json::Value) -> Result<serde_json::Value> + Send + Sync>,
}

// ---------------------------------------------------------------------------
// MCP Server
// ---------------------------------------------------------------------------

/// MCP Server
pub struct McpServer {
tools: Arc<RwLock<HashMap<String, ToolHandler>>>,
resources: Arc<RwLock<HashMap<String, Resource>>>,
}

impl McpServer {
pub fn new() -> Self {
Self {
Expand All @@ -74,6 +103,7 @@ impl McpServer {
tool: Tool,
handler: impl Fn(serde_json::Value) -> Result<serde_json::Value> + Send + Sync + 'static,
) {
tracing::debug!(tool = %tool.name, "registering tool");
self.tools.write().await.insert(
tool.name.clone(),
ToolHandler {
Expand All @@ -84,34 +114,43 @@ impl McpServer {
}

pub async fn register_resource(&self, resource: Resource) {
tracing::debug!(resource = %resource.uri, "registering resource");
self.resources
.write()
.await
.insert(resource.uri.clone(), resource);
}

#[tracing::instrument(skip(self))]
pub async fn list_tools(&self) -> Vec<Tool> {
self.tools
let tools = self
.tools
.read()
.await
.values()
.map(|h| h.tool.clone())
.collect()
.collect::<Vec<_>>();
tracing::debug!(count = tools.len(), "listing tools");
tools
}

#[tracing::instrument(skip(self, arguments))]
pub async fn call_tool(
&self,
name: &str,
arguments: serde_json::Value,
) -> Result<ToolResult, McpError> {
tracing::debug!(tool = %name, "calling tool");
let tools = self.tools.read().await;
let handler = tools
.get(name)
.ok_or(McpError::ToolNotFound(name.to_string()))?;
let handler = tools.get(name).ok_or_else(|| {
tracing::warn!(tool = %name, "tool not found");
McpError::ToolNotFound(name.to_string())
})?;

let result =
(handler.handler)(arguments).map_err(|e| McpError::InvalidRequest(e.to_string()))?;

tracing::info!(tool = %name, "tool called successfully");
Ok(ToolResult {
content: vec![ContentItem {
content_type: "text".to_string(),
Expand All @@ -121,15 +160,21 @@ impl McpServer {
})
}

#[tracing::instrument(skip(self))]
pub async fn list_resources(&self) -> Vec<Resource> {
self.resources.read().await.values().cloned().collect()
let resources: Vec<Resource> = self.resources.read().await.values().cloned().collect();
tracing::debug!(count = resources.len(), "listing resources");
resources
}

#[tracing::instrument(skip(self))]
pub async fn read_resource(&self, uri: &str) -> Result<String, McpError> {
tracing::debug!(resource = %uri, "reading resource");
let resources = self.resources.read().await;
let resource = resources
.get(uri)
.ok_or(McpError::ResourceNotFound(uri.to_string()))?;
let resource = resources.get(uri).ok_or_else(|| {
tracing::warn!(resource = %uri, "resource not found");
McpError::ResourceNotFound(uri.to_string())
})?;
Ok(format!("Resource: {}", resource.name))
}
}
Expand All @@ -140,6 +185,10 @@ impl Default for McpServer {
}
}

// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -212,4 +261,19 @@ mod tests {
assert!(!result.is_error);
assert_eq!(result.content[0].text.as_deref(), Some("42"));
}

// -- new tests for recovery_hint --------------------------------------

#[test]
fn mcp_error_recovery_hints_are_not_empty() {
let variants: [McpError; 3] = [
McpError::ToolNotFound("x".into()),
McpError::ResourceNotFound("x".into()),
McpError::InvalidRequest("x".into()),
];
for v in &variants {
let hint = v.recovery_hint();
assert!(!hint.is_empty(), "empty recovery hint for {v}");
}
}
Comment on lines +267 to +278

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add an FR traceability reference for this new test (and align the traceability table) so the test is mapped to a Functional Requirement now that FUNCTIONAL_REQUIREMENTS.md exists. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

FUNCTIONAL_REQUIREMENTS.md exists, and this newly added test does not reference any FR in the diff. The traceability table is still placeholder content, so the test is not mapped to a Functional Requirement as required.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/mcp-server/src/lib.rs
**Line:** 267:278
**Comment:**
	*Custom Rule: Add an FR traceability reference for this new test (and align the traceability table) so the test is mapped to a Functional Requirement now that FUNCTIONAL_REQUIREMENTS.md exists.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}
1 change: 1 addition & 0 deletions crates/pheno-embedding/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ anyhow = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = "0.1"
reqwest = { version = "0.13", features = ["json", "rustls"] }
76 changes: 67 additions & 9 deletions crates/pheno-embedding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
//!
//! Provides unified interface for embedding generation.

use anyhow::Result;
use serde::{Deserialize, Serialize};
use thiserror::Error;

// ---------------------------------------------------------------------------
// Error types
// ---------------------------------------------------------------------------

#[derive(Error, Debug)]
pub enum EmbeddingError {
#[error("provider error: {0}")]
Expand All @@ -14,6 +17,24 @@ pub enum EmbeddingError {
InvalidInput(String),
}

impl EmbeddingError {
/// Returns a human-readable recovery hint for this error.
pub fn recovery_hint(&self) -> &'static str {
match self {
EmbeddingError::Provider(_) => {
"Check that the embedding provider API key is valid and the service is reachable."
}
EmbeddingError::InvalidInput(_) => {
"Ensure the input text is non-empty and within supported length limits."
}
}
}
Comment on lines +20 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add an explicit AgilePlus spec reference (or link to the created spec item) for this newly introduced error-recovery feature before merging. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This change introduces new error-recovery behavior (recovery_hint) and the file contains no AgilePlus spec reference or traceability marker. That fits the rule requiring new feature code to be backed by an AgilePlus spec item.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/pheno-embedding/src/lib.rs
**Line:** 20:31
**Comment:**
	*Custom Rule: Add an explicit AgilePlus spec reference (or link to the created spec item) for this newly introduced error-recovery feature before merging.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}

// ---------------------------------------------------------------------------
// Core data types
// ---------------------------------------------------------------------------

/// Embedding request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingRequest {
Expand All @@ -34,6 +55,10 @@ pub struct TokenUsage {
pub total_tokens: u32,
}

// ---------------------------------------------------------------------------
// OpenAI embeddings client
// ---------------------------------------------------------------------------

/// OpenAI embeddings client
pub struct OpenAiEmbeddings {
api_key: String,
Expand All @@ -48,6 +73,7 @@ impl OpenAiEmbeddings {
}
}

#[tracing::instrument(skip(self, request))]
pub async fn embed(
&self,
request: &EmbeddingRequest,
Expand All @@ -57,21 +83,41 @@ impl OpenAiEmbeddings {
.clone()
.unwrap_or_else(|| "text-embedding-3-small".to_string());

if request.texts.is_empty() {
return Err(EmbeddingError::InvalidInput(
"texts must not be empty".to_string(),
));
}
Comment on lines +86 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Attach this new request-validation behavior to an AgilePlus work item (or add the missing spec first) so the implementation is not ad hoc. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This adds new request-validation behavior in the embed flow, but there is no accompanying AgilePlus spec reference or creation path in the changed code. The implementation therefore appears untracked as required by the rule.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/pheno-embedding/src/lib.rs
**Line:** 86:90
**Comment:**
	*Custom Rule: Attach this new request-validation behavior to an AgilePlus work item (or add the missing spec first) so the implementation is not ad hoc.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


let body = serde_json::json!({
"input": request.texts,
"model": model,
});

tracing::debug!(
model = %model,
text_count = request.texts.len(),
"sending embedding request"
);

let _response = self
.client
.post("https://api.openai.com/v1/embeddings")
.header("Authorization", format!("Bearer {}", self.api_key))
.json(&body)
.send()
.await
.map_err(|e| EmbeddingError::Provider(e.to_string()))?;
.map_err(|e| {
tracing::error!(error = %e, "embedding request failed");
EmbeddingError::Provider(e.to_string())
})?;
Comment on lines 108 to +113

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The embedding call maps only network-level failures and does not treat non-2xx HTTP responses as errors. As written, provider-side failures can be logged as successful embeddings and returned as Ok. Call error_for_status() (or equivalent status validation) before reporting success. [api mismatch]

Severity Level: Critical 🚨
- ❌ Embedding HTTP errors misreported as successful zero vectors.
- ⚠️ Clients cannot detect provider outages or authorization failures.
Steps of Reproduction ✅
1. Downstream code constructs an `EmbeddingRequest` (e.g., similar to the request shapes
exercised in `crates/pheno-embedding/src/lib.rs:138-156`) and calls
`OpenAiEmbeddings::embed(&request)` from application logic.

2. Inside `embed()` (`crates/pheno-embedding/src/lib.rs:68-127`), the function selects a
model, validates that `request.texts` is non-empty, and builds the embeddings request body
at lines 92-95.

3. At lines 103-109, it sends the HTTP request with
`.post("https://api.openai.com/v1/embeddings").json(&body).send().await`, then maps only
transport-level failures into `EmbeddingError::Provider(e.to_string())` at lines 109-113,
binding `_response` but never calling `error_for_status()` or otherwise checking the
returned HTTP status.

4. When the OpenAI embeddings endpoint responds with a non-2xx status (e.g., `401` for
invalid API key or `429` for rate limiting), `send().await` still returns a
`reqwest::Response`; because status validation is omitted, `embed()` logs `"embedding
successful"` at lines 115-118 and returns dummy zero embeddings at lines 121-125, causing
callers to treat failed embeddings as success and lose the ability to react to
provider-side errors.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/pheno-embedding/src/lib.rs
**Line:** 108:113
**Comment:**
	*Api Mismatch: The embedding call maps only network-level failures and does not treat non-2xx HTTP responses as errors. As written, provider-side failures can be logged as successful embeddings and returned as `Ok`. Call `error_for_status()` (or equivalent status validation) before reporting success.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


tracing::info!(
model = %model,
text_count = request.texts.len(),
"embedding successful"
);

// Simplified response parsing
Ok(EmbeddingResponse {
embeddings: vec![vec![0.0; 1536]; request.texts.len()],
model,
Expand All @@ -80,6 +126,10 @@ impl OpenAiEmbeddings {
}
}

// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -91,7 +141,6 @@ mod tests {
model: Some("text-embedding-3-small".to_string()),
};
let json = serde_json::to_value(&req).unwrap();
// OpenAI-compatible: { "texts": [...], "model": "..." }
assert_eq!(json["texts"][0], "hello");
assert_eq!(json["model"], "text-embedding-3-small");
}
Expand Down Expand Up @@ -144,12 +193,21 @@ mod tests {

#[test]
fn openai_embeddings_new_constructs_without_panic() {
// Constructor stores the api_key and builds a reqwest::Client.
// We don't make a real network call (sandboxed CI would fail).
// If the call were made it would return Err(EmbeddingError::Provider(_))
// since the test key is invalid, but here we just assert construction.
let _c = OpenAiEmbeddings::new("sk-test".to_string());
let _c2 = OpenAiEmbeddings::new("".to_string());
// No panic, no assertion needed beyond construction.
}

// -- new tests for recovery_hint and empty input check -----------------

#[test]
fn embedding_error_recovery_hints_are_not_empty() {
let variants: [EmbeddingError; 2] = [
EmbeddingError::Provider("x".into()),
EmbeddingError::InvalidInput("x".into()),
];
for v in &variants {
let hint = v.recovery_hint();
assert!(!hint.is_empty(), "empty recovery hint for {v}");
}
}
Comment on lines +202 to 212

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add an FR trace reference (for example a Traces to: FR-... comment) to this newly added test to satisfy requirement-to-test traceability. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

FUNCTIONAL_REQUIREMENTS.md exists in the repository, and this newly added test does not reference any FR identifier or trace comment. The test-traceability rule therefore applies.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** crates/pheno-embedding/src/lib.rs
**Line:** 202:212
**Comment:**
	*Custom Rule: Add an FR trace reference (for example a `Traces to: FR-...` comment) to this newly added test to satisfy requirement-to-test traceability.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}
Loading
Loading