Skip to content

feat: mcp servers#7220

Merged
SWvheerden merged 78 commits intotari-project:developmentfrom
fluffypony:mcp_server
Jul 21, 2025
Merged

feat: mcp servers#7220
SWvheerden merged 78 commits intotari-project:developmentfrom
fluffypony:mcp_server

Conversation

@fluffypony
Copy link
Copy Markdown
Contributor

@fluffypony fluffypony commented Jun 14, 2025

Description

This feature introduces a comprehensive implementation of the Model Context Protocol (MCP), enabling secure AI agent integration with the Tari network. It adds three new crates to the workspace:

  1. minotari_mcp_common: A robust, shared framework for building MCP servers. It includes a security-first architecture with local-only binding, permission levels (read-only vs. control), rate limiting, audit logging, input sanitization, and utilities for auto-launching and managing dependent Tari processes.
  2. minotari_mcp_node: A new MCP server application that exposes read-only base node functionality to AI agents. This allows AI to query blockchain state, network status, and mempool information safely.
  3. minotari_mcp_wallet: A new MCP server application that provides AI agents with access to wallet operations. It supports both read-only queries (e.g., checking balance) and protected control operations (e.g., sending funds), with an optional user confirmation layer for added security.

Additionally, this PR includes:

  • A minor fix in the console wallet's gRPC server to correct a filter condition (.clone() was removed from a Copy type).
  • Removal of an unstable Rust feature (internal_output_capture) from the integration tests.
  • Extensive documentation in the main README.md and a new docs/mcp/TARI_MCP_IMPLEMENTATION.md file explaining the architecture and usage.

Motivation and Context

The primary motivation is to allow AI agents (like Claude, GPT-4, etc.) to securely and effectively interact with Tari blockchain functionality. Direct gRPC access is often too complex and risky for AI models. The Model Context Protocol provides a standardized, secure, and agent-friendly interface.

This implementation opens up new possibilities for building AI-powered applications on top of Tari, such as:

  • Automated network monitoring and analysis.
  • AI-driven blockchain exploration and reporting.
  • Controlled wallet management assistants that can query balances and prepare transactions, with final user approval for sending funds.

By providing a structured and secure access layer, we reduce the risk of AI agents making unintended state-changing operations while empowering them with rich data and functionality.

How Has This Been Tested?

  • Manual Server Testing: The new minotari_mcp_node and minotari_mcp_wallet applications have been manually started and tested. This includes verifying their ability to connect to the underlying Tari base node and wallet via gRPC.
  • Auto-Launch Verification: The auto-launch functionality, where the MCP servers start the base node or wallet if not already running, has been tested to ensure correct argument-passing and process supervision.
  • Component Logic: Key components within the minotari_mcp_common framework, such as the security context, registries, and process launchers, have been tested during development.
  • Review of Documentation: The new documentation has been reviewed for accuracy and clarity in explaining the new features and how to use them.

What process can a PR reviewer use to test or verify this change?

  1. Build the new applications:
    cargo build --release -p minotari_mcp_wallet -p minotari_mcp_node
  2. Start the dependent services:
    • In one terminal, start the Tari base node with gRPC enabled:
      ./target/release/minotari_node --enable-grpc
    • In another terminal, start the Tari console wallet with gRPC enabled:
      ./target/release/minotari_console_wallet --enable-grpc
  3. Run the new MCP servers:
    • In a third terminal, start the MCP node server:
      ./target/release/minotari_mcp_node
      Verify it starts and logs that it is connecting to the base node.
    • In a fourth terminal, start the MCP wallet server:
      ./target/release/minotari_mcp_wallet
      Verify it starts and connects to the wallet.
  4. (Optional/Advanced) Interact with a server using a simple JSON-RPC client (e.g., curl). Send a tools/list request to one of the servers and verify it returns a list of available tools.
    • Example for the wallet server (which typically uses stdio, but you can see the logs):
      {"jsonrpc":"2.0","method":"tools/list","id":1}
  5. Review Documentation: Read the new "AI Integration (MCP Servers)" section in README.md and the detailed overview in docs/mcp/TARI_MCP_IMPLEMENTATION.md for correctness and clarity.

Breaking Changes

  • None
  • Requires data directory on base node to be deleted
  • Requires hard fork
  • Other - Please specify

Summary by CodeRabbit

  • New Features
    • Introduced the Minotari Node MCP Server, enabling secure, local-only AI access to Tari blockchain node operations.
    • Added a comprehensive suite of node-specific tools for blockchain queries, mempool analysis, mining operations, network diagnostics, and peer management (ban/unban).
    • Implemented a registry for node resources and prompts, including health, mining setup, peer diagnostics, and sync troubleshooting.
    • Provided configuration, CLI, and process management for flexible server setup and auto-launch capabilities.
    • Integrated robust gRPC client connections, dynamic tool discovery, and health monitoring with circuit breakers.
    • Added process launching and supervision with health checks and graceful shutdown.
    • Developed input sanitization, security context with permission levels and audit logging, and error handling with JSON-RPC integration.
    • Added detailed documentation and usage guidance for installation, configuration, and AI integration.

- Replace Vec::new() + push pattern with vec![] macro
- Use next_back() instead of last() on DoubleEndedIterator
- Replace single-pattern match with if let
- Use range contains instead of manual bounds checking
- Add allow annotations for factory method naming pattern
- Add comprehensive gRPC method discovery with 37+ base node and 25+ wallet methods
- JSON Schema generation for all gRPC methods with validation support
- Method categorization (blockchain, mining, network, mempool, balance, transaction, etc.)
- Base node gRPC configuration parser with method filtering and wildcards
- Support for control operation classification and streaming detection
- Foundation for automated MCP tool generation from gRPC definitions
- Add 20+ base node tools covering all major gRPC methods
- Blockchain tools: headers, blocks, tip info, sync status, network state
- Mempool tools: stats, transactions, analysis, transaction state
- Network tools: status, peers, diagnostics, node identity
- Mining tools: templates, analysis, coinbase support
- Enhanced existing mining tool with better parameter validation
- Organized tools by functional category for maintainability
- All tools include proper error handling and detailed responses
- Add 20+ wallet tools covering all major wallet gRPC methods
- Balance tools: balance info, analysis, monitoring, unspent amounts
- Transaction tools: info, history, transfers, coin splits, analysis
- Address tools: generation, validation, format conversion, payment IDs
- Atomic swap tools: SHA swaps, HTLC claims, refunds, status
- Enhanced existing tools with better validation and detailed responses
- Organized tools by functional category for maintainability
- All tools include proper error handling and user-friendly outputs
- Created GrpcExecutor for unified gRPC method execution
- Added NodeGrpcClient and WalletGrpcClient traits for type-safe operations
- Implemented NodeGrpcClientImpl and WalletGrpcClientImpl concrete clients
- Updated AutoDiscoveryRegistry to support real gRPC execution
- Added real execution logic to DynamicGrpcTool
- Added dependencies for minotari gRPC clients
- Framework foundation complete, compilation errors to fix next
Remove unnecessary blank lines in tool_macros.rs
- Add else blocks to fix else-if-without-else warnings
- Use drop() instead of let _ for cleaner resource handling
- Add allow clippy::too-many-lines for data definition functions
- Fix clone_on_copy warning by removing unnecessary clone()
- Improve code consistency and clarity
Add lint configuration to allow certain style-focused lints that
don't affect functionality:
- too_many_lines: Functions are complex by nature for MCP tools
- if_not_else: Sometimes clearer than suggested alternative
- cast_possible_truncation: Controlled casting for protocol types
- cast_possible_wrap: Blockchain values handled appropriately
- struct_excessive_bools: CLI args naturally have many boolean flags
- else_if_without_else: Not always needed for validation logic
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jun 14, 2025

## Walkthrough

This change introduces the foundational infrastructure for the Minotari Model Context Protocol (MCP) servers, primarily by adding the `minotari_mcp_common` library crate and the `minotari_mcp_node` application. The update includes comprehensive implementations for configuration, error handling, security, tool/resource/prompt registries, gRPC integration, process management, health monitoring, CLI, and extensive documentation. Node-specific tools and resources are also implemented, enabling secure, local-only AI access to Tari node operations.

## Changes

| File(s) / Path(s)                                                                                  | Change Summary                                                                                                                                                                                                                          |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Cargo.toml                                                                                         | Added MCP application directories to workspace members and set up workspace dependency for `minotari_mcp_common`.                                                                                |
| README.md                                                                                          | Updated documentation to include MCP servers, build/run instructions, AI integration, security features, and links to further docs.                                                              |
| applications/minotari_mcp_common/** (all new)                                                      | Added new library crate implementing MCP server infrastructure: configuration, error handling, security, tool/resource/prompt registries, gRPC integration, process management, health monitoring, macros, and documentation.           |
| applications/minotari_mcp_node/** (all new)                                                        | Added new node MCP server application: CLI, configuration, gRPC config parser, main entrypoint, server, node-specific tools, resources, prompts, and documentation.                              |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User/AI Agent
    participant MCP Server (Node)
    participant Tool/Resource/Prompt Registry
    participant SecurityContext
    participant BaseNode gRPC Client
    participant Tari Base Node

    User/AI Agent->>MCP Server (Node): JSON-RPC request (e.g., call tool)
    MCP Server (Node)->>SecurityContext: Check permissions, rate limit, audit log
    alt Permission granted
        MCP Server (Node)->>Tool/Resource/Prompt Registry: Resolve tool/resource/prompt
        Tool/Resource/Prompt Registry->>BaseNode gRPC Client: Prepare and execute gRPC call
        BaseNode gRPC Client->>Tari Base Node: gRPC request
        Tari Base Node-->>BaseNode gRPC Client: gRPC response
        BaseNode gRPC Client-->>Tool/Resource/Prompt Registry: Response
        Tool/Resource/Prompt Registry-->>MCP Server (Node): Result (JSON)
        MCP Server (Node)-->>User/AI Agent: JSON-RPC response
    else Permission denied / error
        MCP Server (Node)-->>User/AI Agent: JSON-RPC error
    end

Estimated code review effort

4 (60–120 minutes) – The changes add a large new library crate and a new application with many modules, complex features, and integrations requiring thorough review.

Possibly related PRs

Poem

🐇✨
New code has hopped into the field,
With tools and prompts, its power revealed.
MCP servers now stand tall,
Node and common, serving all.
Secure and clever, on localhost they run—
A blockchain warren, ready for fun!
🥕


<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=tari-project/tari&utm_content=7220):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 45

♻️ Duplicate comments (1)
applications/minotari_mcp_common/Cargo.toml (1)

13-30: Same unpublished dependency versions as node crate

The tokio 1.44 / tonic 0.13.1 combo here will break the build for the common crate too.
Fix as per previous comment so the workspace compiles on CI.

🧹 Nitpick comments (51)
applications/minotari_mcp_node/src/resources/transaction_info.rs (2)

13-15: Remove the #[allow(dead_code)] once the client is used

The attribute is only needed while the field is unused. It’s easy to forget and leave the suppression in place once the read() method starts querying the node.


62-68: Add a basic connectivity check to surface node errors early

All other resources test the gRPC connection (e.g. get_version) before returning data. Doing the same here keeps behaviour consistent and gives callers immediate feedback when the base node is unavailable.

applications/minotari_mcp_node/src/resources/sync_progress.rs (1)

41-49: mut not required – remove to silence clippy

The cloned client isn’t mutated after creation. Drop the mut to avoid unnecessary warn(clippy::needless_mut) lint.

-        let mut client = self.grpc_client.as_ref().clone();
+        let client = self.grpc_client.as_ref().clone();
applications/minotari_mcp_node/src/resources/mempool_stats.rs (2)

40-49: Eliminate needless mut on the cloned client

Same pattern as other resources – the variable is never mutated.

-        let mut client = self.grpc_client.as_ref().clone();
+        let client = self.grpc_client.as_ref().clone();

51-56: Consider returning null fields instead of hard-coded zeros

Hard-coding 0 may mislead consumers into thinking the mempool is empty rather than “data unavailable”. Using null (or omitting the keys) better signals the placeholder status.

applications/minotari_mcp_node/src/resources/chain_metadata.rs (2)

40-48: Remove unnecessary mut on client

The client isn’t mutated.

-        let mut client = self.grpc_client.as_ref().clone();
+        let client = self.grpc_client.as_ref().clone();

51-61: Avoid duplicating fields at two levels of the JSON

best_block_height, accumulated_difficulty, and pruned_height appear both at the root and inside chain_metadata. Unless consumers need both, keeping a single canonical location prevents divergence.

applications/minotari_mcp_node/src/resources/network_difficulty.rs (2)

40-49: Drop redundant mut on cloned client

Consistent with earlier comments.

-        let mut client = self.grpc_client.as_ref().clone();
+        let client = self.grpc_client.as_ref().clone();

52-57: Return numeric difficulty as well as hex

Many consumers expect the raw u128/Integer difficulty value for comparison. Include both representations to save clients the conversion.

             Ok(serde_json::json!({
                 "accumulated_difficulty": hex::encode(&metadata.accumulated_difficulty),
+                "accumulated_difficulty_numeric": metadata.accumulated_difficulty,
                 "height": metadata.best_block_height,
                 "timestamp": metadata.timestamp,
                 "message": "Network difficulty information"
             }))
applications/minotari_mcp_node/src/resources/block_info.rs (1)

58-64: Stub implementation missing TODO tag

Add an explicit TODO: inside read() so cargo-todo / IDE inspections can surface the needed follow-up.

applications/minotari_mcp_common/src/tool_macros.rs (1)

78-96: required array uses literal duplication – DRY helper would avoid copy-paste

Several schema arms repeat the "required": [...] stanza.
Consider a small helper macro or function to append required fields, reducing maintenance overhead if the schema spec
changes.

README.md (1)

842-883: Minor formatting & spelling fixes in the new MCP section

  • Bullet list after line 848 lacks an empty line above, breaking markdown rendering.
  • “Serviecs” earlier in the doc should be “Services”.
  • Please verify the external link modelcontextprotocol.io – it 404s at the time of writing.
-Applications  
-- **`minotari_mcp_wallet`**: ...
+Applications  
+
+* **`minotari_mcp_wallet`** – Provides secure wallet operations for AI agents
+* **`minotari_mcp_node`** – Enables AI agents to query blockchain and node information
+* **`minotari_mcp_common`** – Shared infrastructure for building secure MCP servers
🧰 Tools
🪛 LanguageTool

[uncategorized] ~848-~848: Loose punctuation mark.
Context: ...pplications - minotari_mcp_wallet: Provides secure wallet operations for A...

(UNLIKELY_OPENING_PUNCTUATION)

applications/minotari_mcp_common/README.md (2)

34-50: Specify a language for the fenced block

Markdown-lint flags this block (```) because it lacks a language identifier.
Add e.g. text to silence MD040 and enable proper rendering.

-```
+```text
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

34-34: Fenced code blocks should have a language specified
null

(MD040, fenced-code-language)


21-23: Hyphenate compound adjective

“per-client request rate limiting” → “per-client request-rate limiting” or simply “per-client rate limiting” to satisfy grammar check and improve clarity.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~21-~21: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...ting**: Configurable per-client request rate limiting - Audit logging: Comprehensive logg...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

applications/minotari_mcp_node/src/tools/mod.rs (1)

89-96: Gate control-only tools behind cfg to avoid unused code

submit_block and submit_transaction (and commented peer-ban tools) are compiled even when control_enabled == false, incurring extra compile time and binary size.
Consider:

#[cfg(feature = "control")]
mod submit_block;
#[cfg(feature = "control")]
mod submit_transaction;

and enable the feature when the flag is true.

applications/minotari_mcp_common/src/stdio_transport.rs (1)

279-282: Leading-zero fix may corrupt string literals

The regex \b0+(\d+)\b runs on the full JSON text and can replace digits inside quoted strings (e.g., phone numbers).
Limit the substitution to numeric tokens outside string contexts or drop this heuristic to avoid data corruption.

applications/minotari_mcp_node/src/main.rs (1)

38-43: Avoid reopening the log file for every entry

OpenOptions::open is called on each log record, causing many syscalls and potential contention.
Keep a single std::fs::File inside FileLogger and write via a Mutex<File> instead.

applications/minotari_mcp_node/src/config.rs (1)

90-94: Clarify “all methods allowed” semantics

Returning an empty HashSet to signify no restriction is easy to misinterpret.
Recommend either:

  1. Return None/Option<HashSet<…>> where None == unrestricted, or
  2. Add a doc comment & rename (e.g. restricted_methods()).

This eliminates accidental misuse in callers that expect “empty == deny all”.

applications/minotari_mcp_common/src/conversion_registry_factory.rs (1)

40-47: Explicit TODO for Miner/Proxy support

ServerType::{Miner, Proxy} silently fall back to the node registry.
This can mask future gaps—callers may assume miner-specific converters exist.

Add a placeholder (or unimplemented!() while behind a feature flag) so missing converters are obvious during integration tests.

applications/minotari_mcp_node/README.md (1)

56-64: Tighten environment-variable list formatting

The bullet list currently renders as a single paragraph because of the missing blank line before it.
Insert a blank line or use markdown tables to improve readability.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~56-~56: Loose punctuation mark.
Context: ...Variables - MINOTARI_NODE_MCP_ENABLED: Enable MCP server - `MINOTARI_NODE_MCP_...

(UNLIKELY_OPENING_PUNCTUATION)

applications/minotari_mcp_node/src/tools/ban_peer.rs (1)

84-88: Guard against fractional-hour precision loss

Casting f64u64 truncates fractions (e.g. 0.5 h → 1800 s is fine, 1.7 h → 6120 s drops 0.2 h).
If sub-second accuracy isn’t needed, document the intentional truncation; otherwise round() before cast.

let duration_secs = (duration_hours * 3600.0).round() as u64;
applications/minotari_mcp_common/src/protobuf_reflector_simple.rs (1)

30-43: Unused descriptor_set parameter lowers clarity

new accepts descriptor_set but never uses it, which may confuse readers and future maintainers expecting parsing logic. Either consume the bytes (even if only to validate they’re non-empty) or drop the parameter for now.

-pub fn new(_descriptor_set: &[u8]) -> McpResult<Self> {
+pub fn new() -> McpResult<Self> {
applications/minotari_mcp_common/src/resources.rs (1)

125-130: Resolved URI is built but silently dropped

read_resource computes _resolved_uri and then ignores it. Either use it (e.g., pass to read()) or remove the dead assignment to avoid misleading future edits.

applications/minotari_mcp_common/src/method_implementations.rs (1)

70-88: Duplicate height-array checks – keep validation single-sourced

convert re-parses and type-checks heights; validate repeats the same loop, doubling maintenance work.

Refactor so convert calls validate first (cheap) and then performs the straightforward extraction, or extract the common logic into a helper.

applications/minotari_mcp_node/src/cli.rs (1)

158-212: Two divergent code-paths build node arguments

generate_node_args hand-assembles the CLI while extract_node_args delegates to NodeArgumentBuilder. Keeping both will drift over time.

Prefer a single source (e.g., wrap NodeArgumentBuilder inside generate_node_args and delete the manual version) to avoid mismatched behaviour.

applications/minotari_mcp_common/src/tools.rs (1)

58-63: Duplicate tool registration not guarded

register blindly inserts into the HashMap, silently overwriting an existing tool with the same name.

Add a check to return an error (or at least log a warning) when the name already exists to avoid accidental shadowing.

applications/minotari_mcp_common/src/transport.rs (1)

170-188: Avoid the extra allocation and shadowing when trimming input.

let line = line.trim(); shadows the String with a &str, triggering a re-allocation on every loop.
Just operate on the original buffer:

if line.trim().is_empty() {
    continue;
}
let slice = line.trim_end();
...
applications/minotari_mcp_common/src/grpc_error_mapper.rs (1)

130-142: Mapping Code::Ok to an error context is misleading.

A gRPC OK status should never be treated as an error; carrying it through the error mapper is confusing and wastes allocations.
Consider returning early or skipping this variant.

applications/minotari_mcp_common/src/process_manager.rs (1)

246-257: Double-kill may panic on some platforms.

After the first kill() succeeds the child is already dead; a second kill can
error with ESRCH. Remove the redundant call or check try_wait() first.

applications/minotari_mcp_common/src/prompts.rs (1)

134-171: Macro leaks SimplePrompt symbol outside caller scope.

Declare it with #[allow(non_camel_case_types)] and wrap in an inner module or
use macro_rules! hygiene (struct $crate::__private::SimplePrompt).
This prevents name clashes when the macro is invoked multiple times in one file.

applications/minotari_mcp_common/src/input_sanitizer.rs (1)

124-126: Simplify the complex character filter for better readability.

The character filter condition is complex and hard to understand. Consider extracting it to a helper function.

-        sanitized = sanitized
-            .chars()
-            .filter(|&c| c == '\n' || c == '\t' || c == '\r' || (c >= ' ' && c != '\u{007F}') || c >= '\u{0080}')
-            .collect();
+        sanitized = sanitized
+            .chars()
+            .filter(|&c| is_allowed_char(c))
+            .collect();

Add this helper function:

/// Check if a character should be allowed in sanitized strings
fn is_allowed_char(c: char) -> bool {
    match c {
        '\n' | '\t' | '\r' => true,  // Allow specific whitespace
        '\u{0020}'..='\u{007E}' => true,  // Printable ASCII (excluding DEL)
        '\u{0080}'.. => true,  // Allow all non-ASCII Unicode
        _ => false,  // Reject other control characters
    }
}
applications/minotari_mcp_common/src/parameter_converter.rs (2)

60-61: Remove unused type alias.

The DynConverter type alias is defined but never used in this file.

-/// Dynamic converter function type for trait object storage
-pub type DynConverter = dyn Fn(Value) -> Result<Box<dyn prost::Message + Send>, ConversionError> + Send + Sync;

82-85: Consider performance implications of blocking async operations.

Using block_in_place with block_on to run async code synchronously can impact performance in high-throughput scenarios as it blocks the thread. Consider either:

  1. Making the registry methods async
  2. Requiring converters to provide both sync and async versions
  3. Document the performance implications

Alternative approach:

// Store both sync and async converters
pub struct ConversionRegistry {
    sync_converters: HashMap<String, Box<dyn Fn(Value) -> Result<Box<dyn prost::Message + Send>, ConversionError> + Send + Sync>>,
    async_converters: HashMap<String, Box<dyn ParameterConverter>>,
}
applications/minotari_mcp_node/src/grpc_config_parser.rs (1)

221-234: Consider making the valid services list configurable.

The hardcoded list of valid services limits extensibility. As new services are added to Tari, this code would need to be updated.

Consider making this configurable:

 fn validate_service_name(&self, service: &str) -> Result<(), GrpcConfigError> {
-    const VALID_SERVICES: &[&str] = &["BaseNode", "Wallet"];
+    // Could be passed in via config or determined from registered services
+    let valid_services = self.get_valid_services();
 
-    if !VALID_SERVICES.contains(&service) {
+    if !valid_services.contains(&service) {
         return Err(GrpcConfigError::UnknownService(format!(
             "Unknown service: {}. Valid services: {}",
             service,
-            VALID_SERVICES.join(", ")
+            valid_services.join(", ")
         )));
     }
 
     Ok(())
 }
applications/minotari_mcp_common/src/response_converter.rs (2)

78-89: Converter registration silently overwrites previous entries

register_converter inserts every method into the same HashMap; if two converters register the same method later registrations silently replace earlier ones.
Consider either:

  • checking/explaining why duplicates cannot happen, or
  • logging / returning an error when a duplicate registration occurs to avoid accidental shadowing.

90-107: No usage metrics for fallback hits

When convert_response falls back because a method‐specific converter is missing you lose visibility – nothing distinguishes a “handled by fallback” case from an intended generic call.
Emit a debug!/warn! (or expose a counter) before returning to make these cases observable.

applications/minotari_mcp_common/src/auto_registry.rs (2)

365-386: Heuristic risk classification is fragile

String-contains checks such as name_lower.contains("send") can mis-classify e.g. resend_* methods.
Regex anchors (e.g. ^send_|_send$) or an explicit allow-list per category would avoid false positives.


460-485: Two read-locks taken independently – race to inconsistent view

get_healthy_tools acquires one lock on tools_cache and another on tool_status; a status change between the two can yield an enabled tool filtered out or vice-versa.
Grabbing both under a single read lock (or cloning status first) would avoid this minor TOCTOU window.

applications/minotari_mcp_common/src/cli_integration.rs (1)

311-334: Address validation too permissive

validate_grpc_address returns true for http://: (host missing) and any string containing one :.
Consider tightening the check with a regex or url::Url parsing to avoid producing invalid endpoints.

applications/minotari_mcp_common/src/grpc_discovery.rs (1)

681-693: Large number of identical json! allocations

All schema helpers create a fresh serde_json::Value every call.
The functions are used as constants – they never mutate – so every GrpcMethodInfo built incurs extra heap work and a parse step.

Using once_cell::sync::Lazy (or const once serde_json::json! becomes const-eval) will:

  • Avoid repeated allocations
  • Cut compile-time code size (the macro expands to a lot of code)

Example:

use once_cell::sync::Lazy;

static EMPTY_SCHEMA: Lazy<Value> = Lazy::new(|| {
    serde_json::json!({
        "type": "object",
        "properties": {},
        "additionalProperties": false
    })
});

fn empty_schema() -> Value {
    EMPTY_SCHEMA.clone()
}

This pattern keeps the ergonomics while being far lighter at runtime.

applications/minotari_mcp_common/src/error.rs (1)

112-125: JSON-RPC error codes are overly reused

Almost every variant maps to -32603 (“Internal error”).
Down-stream clients lose the ability to distinguish authz vs rate-limit vs server errors.

Consider:

  • PermissionDenied / AuthenticationFailed-32001
  • RateLimitExceeded-32010
  • ServerError / TransportError → keep -32603
  • ToolExecutionFailed-32002

Using the reserved server error range (-32000 … -32099) preserves JSON-RPC compliance while conveying intent.

applications/minotari_mcp_common/src/health_checker.rs (1)

282-344: Monitoring loop never exits on channel loss

If the gRPC channel disappears, the task logs a warning and breaks—yet the JoinHandle remains in monitoring_tasks.
Remove the stale entry to prevent memory leaks & false “still-monitored” state:

// after break;
{
    let mut tasks = monitoring_tasks.write().unwrap();
    tasks.remove(&service_name_clone);
}
applications/minotari_mcp_common/src/connection_manager.rs (2)

52-77: Unused configuration fields

failure_window, keep_alive_interval, and keep_alive_timeout are defined but never referenced.
Dead fields complicate maintenance and mislead readers—either wire them into the pool / breaker logic or drop them until needed.


485-497: Connection selection lacks load-balancing

first() always returns the same channel, creating an unintended hotspot and defeating pooling benefits.
Round-robin or random selection is trivial:

- if let Some(conn) = conn_list.first() {
+ if let Some(conn) = conn_list.choose(&mut rand::thread_rng()) {

(rand crate or simple index counter).

applications/minotari_mcp_common/src/grpc_executor.rs (1)

445-454: Health check might mask missing service entry

get_all_health_status().get(service_name) returns None when the service hasn’t been registered. Treating this the same as “healthy” can hide configuration errors. Consider:

let health_opt = conn_manager.get_all_health_status().get(service_name);
match health_opt {
    Some(h) if !h.is_healthy() => { … warn or fail … }
    None => log::warn!("Health status for {} not found", service_name),
    _ => {}
}
applications/minotari_mcp_common/src/process_launcher.rs (3)

92-98: Conflicting timeout sources

HealthMonitor::with_timeout(Duration::from_secs(10)) hard-codes a 10 s per-check timeout, yet health_check_config.max_wait_time below dictates how long we wait overall. This divergence makes tuning tricky and unexpected for operators. Consider deriving the per-check timeout from health_check_config (or exposing both explicitly).


341-345: Lock contention: use a read lock

is_running() acquires a write() lock although it only reads state, blocking concurrent readers unnecessarily. Replace with read() for better concurrency.


270-279: Executable inference is brittle

Heuristics based on substring search ("wallet" / "node") can misclassify (e.g. a custom arg containing “node”). Accept an explicit enum or pass the executable finder result from the caller to avoid surprises.

applications/minotari_mcp_common/src/lib.rs (1)

40-84: Extremely wide re-export surface

Re-exporting almost every internal module at the crate root couples downstream code to internal structure and makes future refactors breaking. Consider grouping into sub-modules (e.g. prelude::*) and exposing only stable, intentional APIs.

applications/minotari_mcp_node/src/prompts/mod.rs (1)

15-19: Consider promoting the registry to a lazy static to avoid repeated allocations.

NodePromptRegistry::new constructs the same PromptRegistry on every call.
If this factory is invoked frequently, use once_cell::sync::Lazy (or similar) to cache the registry.

This is a micro-optimisation, but it eliminates repeated string allocations and hashing.

applications/minotari_mcp_common/src/config.rs (1)

102-115: Minor: derive Default + Eq/PartialEq for testability.

You already implement Default manually; deriving it (along with Eq/PartialEq) improves unit-test ergonomics and removes boilerplate.

No code change strictly required, but worth considering.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ab0cd5 and 9f5f1d4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (61)
  • Cargo.toml (2 hunks)
  • README.md (8 hunks)
  • applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs (1 hunks)
  • applications/minotari_mcp_common/Cargo.toml (1 hunks)
  • applications/minotari_mcp_common/README.md (1 hunks)
  • applications/minotari_mcp_common/src/auto_registry.rs (1 hunks)
  • applications/minotari_mcp_common/src/cli_integration.rs (1 hunks)
  • applications/minotari_mcp_common/src/config.rs (1 hunks)
  • applications/minotari_mcp_common/src/connection_manager.rs (1 hunks)
  • applications/minotari_mcp_common/src/conversion_registry_factory.rs (1 hunks)
  • applications/minotari_mcp_common/src/error.rs (1 hunks)
  • applications/minotari_mcp_common/src/executable_finder.rs (1 hunks)
  • applications/minotari_mcp_common/src/grpc_client_implementations.rs (1 hunks)
  • applications/minotari_mcp_common/src/grpc_discovery.rs (1 hunks)
  • applications/minotari_mcp_common/src/grpc_error_mapper.rs (1 hunks)
  • applications/minotari_mcp_common/src/grpc_executor.rs (1 hunks)
  • applications/minotari_mcp_common/src/health_checker.rs (1 hunks)
  • applications/minotari_mcp_common/src/health_monitor.rs (1 hunks)
  • applications/minotari_mcp_common/src/input_sanitizer.rs (1 hunks)
  • applications/minotari_mcp_common/src/lib.rs (1 hunks)
  • applications/minotari_mcp_common/src/method_implementations.rs (1 hunks)
  • applications/minotari_mcp_common/src/parameter_converter.rs (1 hunks)
  • applications/minotari_mcp_common/src/process_launcher.rs (1 hunks)
  • applications/minotari_mcp_common/src/process_manager.rs (1 hunks)
  • applications/minotari_mcp_common/src/prompts.rs (1 hunks)
  • applications/minotari_mcp_common/src/protobuf_integration.rs (1 hunks)
  • applications/minotari_mcp_common/src/protobuf_reflector.rs (1 hunks)
  • applications/minotari_mcp_common/src/protobuf_reflector_simple.rs (1 hunks)
  • applications/minotari_mcp_common/src/resources.rs (1 hunks)
  • applications/minotari_mcp_common/src/response_converter.rs (1 hunks)
  • applications/minotari_mcp_common/src/schema_generator.rs (1 hunks)
  • applications/minotari_mcp_common/src/security.rs (1 hunks)
  • applications/minotari_mcp_common/src/server.rs (1 hunks)
  • applications/minotari_mcp_common/src/startup_diagnostics.rs (1 hunks)
  • applications/minotari_mcp_common/src/stdio_transport.rs (1 hunks)
  • applications/minotari_mcp_common/src/tool_macros.rs (1 hunks)
  • applications/minotari_mcp_common/src/tool_metadata.rs (1 hunks)
  • applications/minotari_mcp_common/src/tools.rs (1 hunks)
  • applications/minotari_mcp_common/src/transport.rs (1 hunks)
  • applications/minotari_mcp_node/Cargo.toml (1 hunks)
  • applications/minotari_mcp_node/README.md (1 hunks)
  • applications/minotari_mcp_node/src/cli.rs (1 hunks)
  • applications/minotari_mcp_node/src/config.rs (1 hunks)
  • applications/minotari_mcp_node/src/grpc_config_parser.rs (1 hunks)
  • applications/minotari_mcp_node/src/main.rs (1 hunks)
  • applications/minotari_mcp_node/src/prompts/mod.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/block_info.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/chain_metadata.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/mempool_stats.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/mod.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/network_difficulty.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/network_status.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/peer_list.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/sync_progress.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/transaction_info.rs (1 hunks)
  • applications/minotari_mcp_node/src/server.rs (1 hunks)
  • applications/minotari_mcp_node/src/tools/ban_peer.rs (1 hunks)
  • applications/minotari_mcp_node/src/tools/blockchain_tools.rs (1 hunks)
  • applications/minotari_mcp_node/src/tools/mempool_tools.rs (1 hunks)
  • applications/minotari_mcp_node/src/tools/mining_tools.rs (1 hunks)
  • applications/minotari_mcp_node/src/tools/mod.rs (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (9)
applications/minotari_mcp_node/src/resources/peer_list.rs (5)
applications/minotari_mcp_common/src/resources.rs (6)
  • new (62-67)
  • uri (15-15)
  • name (18-18)
  • description (21-21)
  • mime_type (24-24)
  • read (27-27)
applications/minotari_mcp_node/src/prompts/mod.rs (1)
  • new (18-127)
applications/minotari_mcp_node/src/resources/network_status.rs (5)
  • uri (24-26)
  • name (28-30)
  • description (32-34)
  • mime_type (36-38)
  • read (40-58)
applications/minotari_mcp_node/src/resources/sync_progress.rs (5)
  • uri (24-26)
  • name (28-30)
  • description (32-34)
  • mime_type (36-38)
  • read (40-56)
applications/minotari_mcp_common/src/error.rs (1)
  • resource_access_failed (81-83)
applications/minotari_mcp_node/src/resources/transaction_info.rs (9)
applications/minotari_mcp_common/src/resources.rs (8)
  • new (62-67)
  • uri (15-15)
  • name (18-18)
  • description (21-21)
  • mime_type (24-24)
  • supports_templating (30-32)
  • resolve_template (35-41)
  • read (27-27)
applications/minotari_mcp_node/src/resources/block_info.rs (8)
  • new (18-20)
  • uri (25-27)
  • name (29-31)
  • description (33-35)
  • mime_type (37-39)
  • supports_templating (41-43)
  • resolve_template (45-56)
  • read (58-64)
applications/minotari_mcp_node/src/resources/chain_metadata.rs (6)
  • new (17-19)
  • uri (24-26)
  • name (28-30)
  • description (32-34)
  • mime_type (36-38)
  • read (40-70)
applications/minotari_mcp_node/src/resources/network_difficulty.rs (6)
  • new (17-19)
  • uri (24-26)
  • name (28-30)
  • description (32-34)
  • mime_type (36-38)
  • read (40-65)
applications/minotari_mcp_node/src/resources/network_status.rs (5)
  • uri (24-26)
  • name (28-30)
  • description (32-34)
  • mime_type (36-38)
  • read (40-58)
applications/minotari_mcp_node/src/resources/sync_progress.rs (5)
  • uri (24-26)
  • name (28-30)
  • description (32-34)
  • mime_type (36-38)
  • read (40-56)
applications/minotari_mcp_node/src/resources/peer_list.rs (3)
  • uri (24-26)
  • mime_type (36-38)
  • read (40-56)
applications/minotari_mcp_wallet/src/resources/transaction_history.rs (3)
  • uri (23-25)
  • mime_type (35-37)
  • read (39-122)
applications/minotari_mcp_common/src/error.rs (1)
  • invalid_request (61-63)
applications/minotari_mcp_node/src/resources/network_status.rs (7)
applications/minotari_mcp_node/src/prompts/mod.rs (1)
  • new (18-127)
applications/minotari_mcp_node/src/resources/network_difficulty.rs (6)
  • new (17-19)
  • uri (24-26)
  • name (28-30)
  • description (32-34)
  • mime_type (36-38)
  • read (40-65)
applications/minotari_mcp_common/src/resources.rs (5)
  • uri (15-15)
  • name (18-18)
  • description (21-21)
  • mime_type (24-24)
  • read (27-27)
applications/minotari_mcp_node/src/resources/mempool_stats.rs (3)
  • uri (24-26)
  • mime_type (36-38)
  • read (40-57)
applications/minotari_mcp_node/src/resources/sync_progress.rs (3)
  • uri (24-26)
  • mime_type (36-38)
  • read (40-56)
applications/minotari_mcp_node/src/resources/peer_list.rs (3)
  • uri (24-26)
  • mime_type (36-38)
  • read (40-56)
applications/minotari_mcp_common/src/error.rs (1)
  • resource_access_failed (81-83)
applications/minotari_mcp_node/src/resources/network_difficulty.rs (6)
applications/minotari_mcp_node/src/resources/chain_metadata.rs (6)
  • new (17-19)
  • uri (24-26)
  • name (28-30)
  • description (32-34)
  • mime_type (36-38)
  • read (40-70)
applications/minotari_mcp_node/src/tools/mod.rs (1)
  • new (54-98)
applications/minotari_mcp_common/src/resources.rs (3)
  • uri (15-15)
  • mime_type (24-24)
  • read (27-27)
applications/minotari_mcp_node/src/resources/network_status.rs (5)
  • uri (24-26)
  • name (28-30)
  • description (32-34)
  • mime_type (36-38)
  • read (40-58)
applications/minotari_mcp_node/src/resources/peer_list.rs (3)
  • uri (24-26)
  • mime_type (36-38)
  • read (40-56)
applications/minotari_mcp_common/src/error.rs (1)
  • resource_access_failed (81-83)
applications/minotari_mcp_node/src/resources/mod.rs (5)
applications/minotari_mcp_common/src/resources.rs (1)
  • new (62-67)
applications/minotari_mcp_node/src/prompts/mod.rs (1)
  • new (18-127)
applications/minotari_mcp_node/src/tools/mod.rs (1)
  • new (54-98)
applications/minotari_mcp_node/src/server.rs (2)
  • new (48-50)
  • new (85-178)
applications/minotari_mcp_wallet/src/resources/mod.rs (1)
  • new (18-25)
applications/minotari_mcp_common/src/transport.rs (3)
applications/minotari_mcp_common/src/stdio_transport.rs (3)
  • serde_json (191-191)
  • serde_json (199-199)
  • handle_message (320-326)
applications/minotari_mcp_common/src/server.rs (2)
  • handle_message (178-193)
  • new (57-64)
applications/minotari_mcp_common/src/error.rs (1)
  • invalid_request (61-63)
applications/minotari_mcp_common/src/parameter_converter.rs (2)
applications/minotari_mcp_common/src/error.rs (1)
  • tool_execution_failed (73-75)
applications/minotari_mcp_common/src/method_implementations.rs (15)
  • method_name (18-20)
  • method_name (38-40)
  • method_name (96-98)
  • method_name (114-116)
  • method_name (153-155)
  • convert (22-25)
  • convert (42-68)
  • convert (100-102)
  • convert (118-120)
  • convert (157-159)
  • validate (27-30)
  • validate (70-88)
  • validate (104-106)
  • validate (122-124)
  • validate (161-163)
applications/minotari_mcp_common/src/process_launcher.rs (6)
applications/minotari_mcp_common/src/cli_integration.rs (7)
  • new (39-48)
  • new (112-118)
  • new (213-219)
  • build (80-89)
  • build (165-202)
  • build (268-303)
  • default (357-359)
applications/minotari_mcp_common/src/executable_finder.rs (3)
  • new (24-30)
  • find_wallet (295-297)
  • find_node (290-292)
applications/minotari_mcp_common/src/health_monitor.rs (1)
  • new (50-57)
applications/minotari_mcp_node/src/server.rs (4)
  • new (48-50)
  • new (85-178)
  • stop (188-201)
  • is_running (205-207)
applications/minotari_mcp_wallet/src/server.rs (2)
  • new (38-71)
  • stop (81-94)
applications/minotari_mcp_common/src/error.rs (2)
  • server_error (85-87)
  • config_error (77-79)
applications/minotari_mcp_common/src/error.rs (4)
applications/minotari_mcp_common/src/stdio_transport.rs (2)
  • serde_json (191-191)
  • serde_json (199-199)
applications/minotari_mcp_common/src/transport.rs (1)
  • serde_json (193-193)
applications/minotari_mcp_common/src/parameter_converter.rs (1)
  • from (42-44)
applications/minotari_mcp_common/src/grpc_client_implementations.rs (1)
  • from (24-26)
🪛 LanguageTool
applications/minotari_mcp_node/README.md

[uncategorized] ~56-~56: Loose punctuation mark.
Context: ...Variables - MINOTARI_NODE_MCP_ENABLED: Enable MCP server - `MINOTARI_NODE_MCP_...

(UNLIKELY_OPENING_PUNCTUATION)


[grammar] ~200-~200: You’ve repeated a verb. Did you mean to only write one of them?
Context: ...information. ## Use Cases ### Network Monitoring - Monitor node connectivity and peer health - Tra...

(REPEATED_VERBS)

applications/minotari_mcp_common/README.md

[uncategorized] ~21-~21: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...ting**: Configurable per-client request rate limiting - Audit logging: Comprehensive logg...

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[uncategorized] ~325-~325: Loose punctuation mark.
Context: ...ri_mcp_wallet](../minotari_mcp_wallet/): Wallet MCP server - [minotari_mcp_node...

(UNLIKELY_OPENING_PUNCTUATION)

README.md

[uncategorized] ~848-~848: Loose punctuation mark.
Context: ...pplications - minotari_mcp_wallet: Provides secure wallet operations for A...

(UNLIKELY_OPENING_PUNCTUATION)

🪛 markdownlint-cli2 (0.17.2)
applications/minotari_mcp_common/README.md

34-34: Fenced code blocks should have a language specified
null

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: test (mainnet, stagenet)
  • GitHub Check: test (testnet, esmeralda)
  • GitHub Check: test (nextnet, nextnet)
  • GitHub Check: cargo check with stable
  • GitHub Check: Cucumber tests / FFI
  • GitHub Check: Cucumber tests / Base Layer
🔇 Additional comments (12)
applications/minotari_console_wallet/src/grpc/wallet_grpc_server.rs (1)

1233-1237:

Details

❌ Incorrect review comment

Bit-shift risks remain after removing the .clone()

Good call dropping the superfluous clone, but note that
1 << (tx.status as u32) will panic in debug builds if tx.status ≥ 32 (it’s undefined in release).
Unless the TransactionStatus enum is guaranteed to stay < 32, wrap the shift with checked_shl
or upgrade to a wider integer to prevent accidental crashes.

- .filter(|tx| req.status_bitflag == 0 || (req.status_bitflag & (1 << (tx.status as u32))) != 0)
+ .filter(|tx| {
+     req.status_bitflag == 0
+         || tx.status
+             .try_into()
+             .ok()
+             .and_then(|v: u32| 1u128.checked_shl(v))
+             .map(|mask| (req.status_bitflag as u128 & mask) != 0)
+             .unwrap_or(false)
+ })

🏁 Script executed:

#!/bin/bash
# Locate TransactionStatus enum definition to inspect variant values
rg -n "enum TransactionStatus" -A 20

Length of output: 5656


No panic risk: tx.status values are 0–8, so shifts stay within bounds
After inspecting TransactionStatus (variants 0 through 8) and noting that req.status_bitflag is a u64, the expression

req.status_bitflag == 0
    || (req.status_bitflag & (1 << (tx.status as u32))) != 0

always shifts by less than 64 bits and will never panic. The existing code is safe as-is—no checked_shl wrap or wider integer is required.

Likely an incorrect or invalid review comment.

Cargo.toml (1)

41-44: Workspace members registered correctly

The new MCP crates are wired into the workspace – no issues spotted here.

applications/minotari_mcp_common/src/tool_macros.rs (1)

187-197: Dead-code notice contradicts intent – remove or gate behind feature

Comment says “macros are no longer used in production code” yet the file is shipped and exported.
If truly obsolete, delete the file; otherwise drop the disclaimer to avoid confusing future maintainers.

applications/minotari_mcp_node/src/resources/mod.rs (1)

43-46: Templated resources may need pattern registration

BlockInfoResource and TransactionInfoResource support templating, but they’re registered the same way as static
resources.
Ensure ResourceRegistry::register() handles URI patterns; if not, they will shadow each other and template resolution
will fail.

applications/minotari_mcp_common/src/grpc_client_implementations.rs (1)

49-53: ⚠️ Potential issue

Replace panic with error return in deprecated method.

Panicking in a deprecated method is too harsh and could crash the application unexpectedly. Consider returning an error instead.

 #[deprecated(note = "Use new() with real client")]
 pub fn new_placeholder() -> Self {
-    // This will panic if called - placeholder is deprecated
-    panic!("Placeholder client deprecated - use real client")
+    // Return a dummy client that will fail on use
+    Self {
+        client: BaseNodeGrpcClient::new(Channel::from_static("http://[::]:1").connect_lazy()),
+        conversion_registry: Arc::new(ConversionRegistry::new()),
+    }
 }

Likely an incorrect or invalid review comment.

applications/minotari_mcp_common/src/input_sanitizer.rs (1)

205-207: Consider additional validation for /tmp/ paths.

While allowing /tmp/ paths is common for temporary files, it could still pose security risks if not properly controlled. Consider adding additional validation such as:

  • Ensuring the path doesn't escape /tmp/ using symlinks
  • Validating the path doesn't contain special characters
  • Adding a configurable whitelist of allowed subdirectories under /tmp/
applications/minotari_mcp_common/src/health_monitor.rs (1)

149-151: Channel creation approach is acceptable for health checks.

While creating and immediately dropping a channel is not the most efficient approach, it's acceptable for health monitoring purposes where the frequency is typically low. The comment acknowledges this limitation.

applications/minotari_mcp_node/src/grpc_config_parser.rs (1)

1-472: Well-structured configuration parser with excellent test coverage.

The implementation is comprehensive, handling various method specification formats, wildcards, and providing useful presets. The test coverage is thorough.

applications/minotari_mcp_node/src/tools/blockchain_tools.rs (1)

583-593: Likely wrong protobuf request type

GetTokensInCirculationTool builds a GetBlocksRequest but calls get_tokens_in_circulation.
Unless the gRPC service really expects GetBlocksRequest, this will not compile or will fail at runtime.
Double-check the generated proto – there usually is a dedicated GetTokensInCirculationRequest.

applications/minotari_mcp_node/src/server.rs (1)

215-223: gRPC endpoint lacks explicit connect timeout & scheme validation

Endpoint::from_shared(endpoint_url) will happily accept an URL without scheme (127.0.0.1:18142) and then fail later at dial time.
Also, timeout() sets per-RPC deadlines, not the TCP/TLS handshake timeout.

 let endpoint = Endpoint::from_shared(endpoint_url)
     .map_err(|e| McpError::config_error(format!("Invalid gRPC endpoint: {}", e)))?
-    .timeout(Duration::from_secs(config.node_grpc.timeout_secs));
+    .timeout(Duration::from_secs(config.node_grpc.timeout_secs))       // per-RPC
+    .connect_timeout(Duration::from_secs(config.node_grpc.timeout_secs)) // handshake
+    .tcp_nodelay(true);

You may also want to prepend http:// if the user omitted the scheme and fail fast otherwise.

applications/minotari_mcp_common/src/process_launcher.rs (1)

437-448: convert_to_multiaddr lacks IPv6 / unix-socket support

The helper assumes IPv4 ip:port and blindly splits on the first :, so [::1]:18142 or /ip6/ multiaddrs break. Consider using multiaddr crate parsing or at least detect IPv6 brackets before splitting.

applications/minotari_mcp_node/src/prompts/mod.rs (1)

21-48: Verify that every referenced resource/tool is actually registered.

The prompts rely on resources such as chain_metadata, network_status, sync_progress, mempool_stats, peer_list, and network_difficulty.
A typo or a missing registry entry will surface only at runtime and will cripple the assistant’s response.

Please double-check the resource and tool registries for matching identifiers (case-sensitive).
If you’re willing, an automated compile-time check could be added to the macro to avoid silent drift.

Also applies to: 50-74, 75-98, 100-124

Comment on lines +31 to +40
tokio = { version = "1.44", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
thiserror = "1.0"
clap = { version = "3.2", features = ["derive", "env"] }
log = "0.4"
async-trait = "0.1"
tonic = { version = "0.13.1", default-features = false, features = ["codegen", "transport", "tls-ring"] }
uuid = { version = "1.0", features = ["v4"] }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Crate versions point to non-existent releases – build will fail

tokio = "1.44" and tonic = "0.13.1" are not on crates.io at time of writing (latest ≈ 1.37 and 0.11).
Cargo will error out for the whole workspace.

-tokio = { version = "1.44", features = ["full"] }
-tonic = { version = "0.13.1", default-features = false, features = ["codegen", "transport", "tls-ring"] }
+tokio = { version = "1.37", features = ["full"] }   # align with workspace baseline
+tonic = { version = "0.11", default-features = false, features = ["codegen", "transport", "tls-ring"] }

Please align these with published versions (and update minotari_mcp_common accordingly) or switch to
workspace = true if you already have pinned versions at the root.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tokio = { version = "1.44", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
thiserror = "1.0"
clap = { version = "3.2", features = ["derive", "env"] }
log = "0.4"
async-trait = "0.1"
tonic = { version = "0.13.1", default-features = false, features = ["codegen", "transport", "tls-ring"] }
uuid = { version = "1.0", features = ["v4"] }
tokio = { version = "1.37", features = ["full"] } # align with workspace baseline
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
thiserror = "1.0"
clap = { version = "3.2", features = ["derive", "env"] }
log = "0.4"
async-trait = "0.1"
tonic = { version = "0.11", default-features = false, features = ["codegen", "transport", "tls-ring"] }
uuid = { version = "1.0", features = ["v4"] }
🤖 Prompt for AI Agents
In applications/minotari_mcp_node/Cargo.toml between lines 31 and 40, the
versions specified for the tokio and tonic crates (1.44 and 0.13.1) do not exist
on crates.io, causing build failures. To fix this, update these dependencies to
the latest published versions available on crates.io (e.g., tokio around 1.37
and tonic around 0.11) or configure the workspace to use a shared version by
setting workspace = true and managing versions centrally in the root Cargo.toml.
Also, ensure minotari_mcp_common is updated to match these versions if
applicable.

Comment on lines +51 to +55
// TODO: Replace with actual peer list calls when available
Ok(serde_json::json!({
"peers": [],
"message": "Peer list information - placeholder implementation"
}))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Resource still returns a placeholder – hook up real peer data

/peer_list currently always answers with an empty array.
minotari_app_grpc::tari_rpc::wallet_server::Wallet::list_connected_peers
(or the base-node equivalent) is available – call it and transform the response
so prompts relying on this resource get meaningful data.

Also consider caching the result for a few seconds to avoid hammering the node.

🤖 Prompt for AI Agents
In applications/minotari_mcp_node/src/resources/peer_list.rs around lines 51 to
55, the peer list endpoint currently returns a static empty array as a
placeholder. Replace this placeholder by calling
minotari_app_grpc::tari_rpc::wallet_server::Wallet::list_connected_peers (or the
base-node equivalent) to fetch the actual connected peers. Transform the
response into the expected JSON format and implement caching for a few seconds
to reduce load on the node.

Comment on lines +45 to +57
fn resolve_template(&self, params: &HashMap<String, String>) -> McpResult<String> {
let hash = params
.get("hash")
.ok_or_else(|| McpError::invalid_request("Missing hash parameter"))?;

// Validate hash is hex
if !hash.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(McpError::invalid_request("Hash must be hexadecimal"));
}

if hash.is_empty() {
return Err(McpError::invalid_request("Hash cannot be empty"));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Tighten hash validation and avoid false-positives

  1. hash.chars().all(|c| c.is_ascii_hexdigit()) accepts the string "" because the predicate is vacuously true. Although the subsequent is_empty() check catches this, flipping the order makes the intent clearer.
  2. The check also accepts the "0x" prefix and odd-length hashes. If the node expects a 32-byte (64-char) hex digest without the prefix, enforcing length early prevents malformed requests reaching lower layers.
-        // Validate hash is hex
-        if !hash.chars().all(|c| c.is_ascii_hexdigit()) {
-            return Err(McpError::invalid_request("Hash must be hexadecimal"));
-        }
-
-        if hash.is_empty() {
-            return Err(McpError::invalid_request("Hash cannot be empty"));
-        }
+        if hash.is_empty() {
+            return Err(McpError::invalid_request("Hash cannot be empty"));
+        }
+
+        // Require exactly 64 hex chars (32-byte digest) – adjust if the spec differs
+        if hash.len() != 64 || !hash.chars().all(|c| c.is_ascii_hexdigit()) {
+            return Err(McpError::invalid_request("Hash must be a 64-character hexadecimal string"));
+        }
🤖 Prompt for AI Agents
In applications/minotari_mcp_node/src/resources/transaction_info.rs around lines
45 to 57, the current validation checks the hash for hexadecimal characters
before checking if it is empty, which can cause confusion since an empty string
passes the hex check vacuously. Reorder the checks to first verify that the hash
is not empty. Additionally, add validation to reject hashes with a "0x" prefix
and enforce that the hash length is exactly 64 characters to ensure it is a
valid 32-byte hex digest. This will prevent malformed hashes from proceeding
further.

Comment on lines +52 to +57
Ok(serde_json::json!({
"status": "online",
"node_version": version.value,
"network": "unknown",
"message": "Network status information - placeholder implementation"
}))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Placeholder returns hard-coded status → risk of misleading callers

"status": "online" and "network": "unknown" are always returned, even when the node is out of sync or on a testnet.
Consider:

  1. Translate connectivity outcome into online/offline.
  2. Populate network from get_version().network once available, or omit the field until then.

Failing to do so will give an AI agent a false sense of liveness.

🤖 Prompt for AI Agents
In applications/minotari_mcp_node/src/resources/network_status.rs around lines
52 to 57, the JSON response always returns a hard-coded "status": "online" and
"network": "unknown", which can mislead callers about the actual node state.
Update the code to determine the real connectivity status and set "status" to
"online" or "offline" accordingly. Also, retrieve the actual network value from
get_version().network if available, or omit the "network" field until it can be
accurately populated.

Comment on lines +107 to +110
"timestamp": 0, // Timestamp not available in NewBlockHeaderTemplate
},
"body": {
"inputs": template.body.as_ref().map(|b| b.inputs.len()).unwrap_or(0),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid placeholder timestamps

Hard-coding "timestamp": 0 leaks a meaningless value into the API and may mislead downstream logic that expects a Unix epoch. Either:

  1. Expose the actual header timestamp when NewBlockHeaderTemplate is extended, or
  2. Omit the field (serde_json::Value::Null) until the data is available.

Returning 0 silently is almost always the wrong default for a timestamp.

🤖 Prompt for AI Agents
In applications/minotari_mcp_node/src/tools/mining_tools.rs around lines 107 to
110, the code sets the "timestamp" field to a hardcoded 0, which is misleading
as it does not represent a valid timestamp. To fix this, either update the
NewBlockHeaderTemplate to include the actual timestamp and use it here, or if
the timestamp is not yet available, set the field to serde_json::Value::Null to
omit it from the output instead of using 0.

Comment on lines +64 to +77
let algo = params.get("algo").and_then(|v| v.as_u64()).ok_or_else(|| {
McpError::invalid_request("algo is required (0=SHA3X, 1=RANDOMXM, 2=RANDOMXT)".to_string())
})?;

let max_weight = params.get("max_weight").and_then(|v| v.as_u64()).unwrap_or(19500); // Default max weight for blocks

let pow_algo_val = match algo {
0 => PowAlgos::Randomxm.into(), // RANDOMXM
1 => PowAlgos::Sha3x.into(), // SHA3X
2 => PowAlgos::Randomxt.into(), // RANDOMXT
_ => {
return Err(McpError::invalid_request(
"Invalid algo: must be 0 (RANDOMXM), 1 (SHA3X), or 2 (RANDOMXT)".to_string(),
))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Inconsistent algorithm mapping string → enum → error text

The mapping table (0 => Randomxm, 1 => Sha3x, 2 => Randomxt) is correct, but the error message at line 65 describes a different order (0=SHA3X, 1=RANDOMXM …). Pick one convention and use it in all:

-        McpError::invalid_request("algo is required (0=SHA3X, 1=RANDOMXM, 2=RANDOMXT)".to_string())
+        McpError::invalid_request("algo is required (0=RANDOMXM, 1=SHA3X, 2=RANDOMXT)".to_string())

Leaving it as-is will confuse API consumers and testers.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let algo = params.get("algo").and_then(|v| v.as_u64()).ok_or_else(|| {
McpError::invalid_request("algo is required (0=SHA3X, 1=RANDOMXM, 2=RANDOMXT)".to_string())
})?;
let max_weight = params.get("max_weight").and_then(|v| v.as_u64()).unwrap_or(19500); // Default max weight for blocks
let pow_algo_val = match algo {
0 => PowAlgos::Randomxm.into(), // RANDOMXM
1 => PowAlgos::Sha3x.into(), // SHA3X
2 => PowAlgos::Randomxt.into(), // RANDOMXT
_ => {
return Err(McpError::invalid_request(
"Invalid algo: must be 0 (RANDOMXM), 1 (SHA3X), or 2 (RANDOMXT)".to_string(),
))
let algo = params.get("algo").and_then(|v| v.as_u64()).ok_or_else(|| {
McpError::invalid_request("algo is required (0=RANDOMXM, 1=SHA3X, 2=RANDOMXT)".to_string())
})?;
let max_weight = params.get("max_weight").and_then(|v| v.as_u64()).unwrap_or(19500); // Default max weight for blocks
let pow_algo_val = match algo {
0 => PowAlgos::Randomxm.into(), // RANDOMXM
1 => PowAlgos::Sha3x.into(), // SHA3X
2 => PowAlgos::Randomxt.into(), // RANDOMXT
_ => {
return Err(McpError::invalid_request(
"Invalid algo: must be 0 (RANDOMXM), 1 (SHA3X), or 2 (RANDOMXT)".to_string(),
))
🤖 Prompt for AI Agents
In applications/minotari_mcp_node/src/tools/mining_tools.rs around lines 64 to
77, the numeric to algorithm enum mapping and the error message describing the
valid values are inconsistent. Update the error message at line 65 to match the
actual mapping used in the code: 0 should correspond to RANDOMXM, 1 to SHA3X,
and 2 to RANDOMXT. Ensure the error message clearly reflects this order to avoid
confusion for API consumers.

Comment on lines +6 to +10
use std::{
collections::HashMap,
sync::{Arc, RwLock},
time::{Duration, Instant},
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Blocking locks inside async code

The module relies on std::sync::RwLock, but all access happens inside async tasks.
std locks block the entire Tokio worker thread on contention; use tokio::sync::RwLock (or parking_lot) to avoid priority-inversion and runtime warnings:

-use std::sync::{Arc, RwLock};
+use tokio::sync::RwLock;

Refactor the handful of write().unwrap() / read().unwrap() calls accordingly.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/health_checker.rs around lines 6 to 10,
replace the use of std::sync::RwLock with tokio::sync::RwLock to avoid blocking
the Tokio async runtime. Update all instances of write().unwrap() and
read().unwrap() to use the async lock acquisition methods provided by
tokio::sync::RwLock, such as write().await and read().await, refactoring the
code to be async where necessary.

Comment on lines +200 to +204
fn basic_parameter_validation(&self, params: &Value) -> McpResult<()> {
// Basic validation: ensure it's an object
if !params.is_object() {
return Err(McpError::invalid_request("Parameters must be an object"));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Compilation error – invalid_request expects a String

invalid_request takes a String, but a &str is passed:

-return Err(McpError::invalid_request("Parameters must be an object"));
+return Err(McpError::invalid_request("Parameters must be an object".to_string()));

This breaks the build.

🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/protobuf_integration.rs around lines 200
to 204, the call to McpError::invalid_request currently passes a &str, but the
function expects a String. Fix this by converting the string literal to a String
using .to_string() or String::from() before passing it to invalid_request.

Comment on lines +171 to +179
let output_buffer = self.output_buffer.clone();
tokio::spawn(async move {
let reader = BufReader::new(stdout);
let mut lines = reader.lines();
while let Ok(Some(line)) = lines.next_line().await {
log::debug!("STDOUT: {}", line);
output_buffer.write().await.push(format!("STDOUT: {}", line));
}
log::debug!("STDOUT capture ended");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Unbounded in-memory log capture

Every stdout/stderr line is pushed onto output_buffer, which is never trimmed. A chatty process can exhaust memory or slow the node. Keep only the last n KB/lines, or flush to disk.

Also applies to: 185-191

🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/process_launcher.rs around lines 171 to
179 and 185 to 191, the output_buffer accumulates all stdout/stderr lines
without limit, risking excessive memory use. Modify the code to limit the size
of output_buffer by keeping only the last n lines or n KB of logs. Implement
logic to trim or discard older entries when the buffer exceeds this limit to
prevent unbounded memory growth.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment on lines +34 to +36
pub startup_timeout: Duration,
/// Health check configuration
pub health_check_config: HealthCheckConfig,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

startup_timeout is never referenced

LaunchConfig.startup_timeout is set by the builder but never consulted by ProcessLauncher. Either wire it into the launch/health-wait logic or drop the field to avoid misleading configuration knobs.

🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/process_launcher.rs around lines 34 to
36, the startup_timeout field in LaunchConfig is set but never used in
ProcessLauncher. To fix this, either integrate startup_timeout into the process
launch or health check waiting logic to control timeout behavior, or remove the
startup_timeout field entirely if it is not needed to avoid confusion from
unused configuration options.

Comment on lines +289 to +293
// Try graceful shutdown first (SIGTERM)
match child.kill().await {
Ok(_) => log::info!("Sent SIGTERM to process"),
Err(e) => log::warn!("Failed to send SIGTERM: {}", e),
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

“Graceful” shutdown actually sends SIGKILL

Child::kill() maps to SIGKILL on Unix, yet the log states “Sent SIGTERM”. This prevents the process from performing cleanup and makes the two-phase TERM/KILL logic moot (second kill is redundant). Use nix::sys::signal::kill(pid, SIGTERM) (or platform abstraction) first, then fall back to SIGKILL.

Also applies to: 317-324

🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/process_launcher.rs around lines 289 to
293, the code calls Child::kill() which sends SIGKILL but logs it as SIGTERM,
causing confusion and bypassing graceful shutdown. Replace the call to
Child::kill() with sending SIGTERM explicitly using nix::sys::signal::kill(pid,
SIGTERM) or a platform abstraction, then only call Child::kill() to send SIGKILL
if the process does not terminate after SIGTERM. Apply the same fix to lines
317-324.

Comment on lines +170 to +176
/// Get audit log path with defaults
pub fn get_audit_log_path(&self) -> Option<PathBuf> {
if self.audit_logging {
self.audit_log_path.clone().or_else(|| {
// Default audit log path
Some(PathBuf::from("logs/mcp_audit.log"))
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Ensure the audit-log directory exists or return an error early.

get_audit_log_path silently returns a path that may be unwritable at runtime, leading to I/O errors on the first logged event.
Either:

  1. Create the parent directory here, or
  2. Extend validate() to check writability.

Fail fast rather than at the first log write.

🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/config.rs around lines 170 to 176, the
get_audit_log_path method returns a default audit log path without ensuring the
directory exists or is writable, which can cause runtime I/O errors. To fix
this, modify the method to check if the parent directory of the audit log path
exists and create it if necessary, returning an error early if creation fails.
Alternatively, add a writability check for the audit log path in the validate()
method to fail fast before runtime logging attempts.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Jun 14, 2025

Test Results (CI)

    3 files    132 suites   1h 0m 53s ⏱️
1 427 tests 1 424 ✅ 0 💤 3 ❌
4 279 runs  4 270 ✅ 0 💤 9 ❌

For more details on these failures, see this check.

Results for commit 088fd74.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
applications/minotari_mcp_common/src/process_launcher.rs (4)

170-179: Unbounded memory growth in output_buffer still not addressed

Each captured stdout line is appended to an ever-growing Vec<String> with no eviction strategy. A long-running or chatty node can exhaust memory. This is the exact issue flagged in the previous review and remains unresolved.


34-34: startup_timeout is dead code

The field is defined but never consulted anywhere in the launcher logic, exactly as noted in the previous review. Either integrate it into the launch/health-wait flow or drop it to avoid a misleading knob.


289-293: “Graceful” shutdown still sends SIGKILL while claiming SIGTERM

Child::kill() maps to SIGKILL on Unix, yet the log says “Sent SIGTERM”. The graceful/force-kill two-phase logic therefore never actually sends SIGTERM and the second kill is redundant.


170-191: ⚠️ Potential issue

Limit captured output to a bounded ring buffer

If you keep in-memory capture, trim the buffer after each push:

-output_buffer.write().await.push(format!("STDOUT: {}", line));
+{
+    const MAX_LINES: usize = 1_000;
+    let mut buf = output_buffer.write().await;
+    buf.push(format!("STDOUT: {}", line));
+    if buf.len() > MAX_LINES {
+        buf.remove(0); // discard oldest
+    }
+}

Apply the same pattern to the stderr block.

🧹 Nitpick comments (2)
applications/minotari_mcp_common/src/process_launcher.rs (2)

341-345: Unnecessary exclusive lock; use read() instead of write()

is_running() acquires a write lock just to call try_wait(). A read lock suffices and avoids blocking other writers:

-pub async fn is_running(&self) -> bool {
-    if let Some(child) = self.process.write().await.as_mut() {
+pub async fn is_running(&self) -> bool {
+    if let Some(child) = self.process.read().await.as_ref() {

218-222: Duplicate Running status notifications

ProcessLaunchStatus::Running is emitted once in line 218 and again in line 248. Remove the second send to avoid redundant updates.

Also applies to: 248-249

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f5f1d4 and 088fd74.

📒 Files selected for processing (1)
  • applications/minotari_mcp_common/src/process_launcher.rs (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: test (mainnet, stagenet)
  • GitHub Check: test (nextnet, nextnet)
  • GitHub Check: test (testnet, esmeralda)
  • GitHub Check: ci
  • GitHub Check: cargo check with stable

@github-actions
Copy link
Copy Markdown

Test Results (Integration tests)

 2 files  + 2  10 suites  +10   2h 10m 52s ⏱️ + 2h 10m 52s
27 tests +27  20 ✅ +20  1 💤 +1   6 ❌ + 6 
38 runs  +38  21 ✅ +21  1 💤 +1  16 ❌ +16 

For more details on these failures, see this check.

Results for commit 088fd74. ± Comparison against base commit 7ab0cd5.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
applications/minotari_mcp_common/src/process_launcher.rs (3)

34-36: Remove unused startup_timeout field

The startup_timeout field is configured via the builder but never used in the actual process launching logic. This creates confusion about what timeouts are actually applied.


171-181: Implement bounded output buffer to prevent memory exhaustion

The output buffer accumulates all stdout/stderr lines indefinitely, which could lead to memory exhaustion with verbose processes.

Consider implementing a circular buffer or limiting the buffer size:

// In the struct definition
- output_buffer: Arc<RwLock<Vec<String>>>,
+ output_buffer: Arc<RwLock<std::collections::VecDeque<String>>>,
+ max_buffer_size: usize, // Add this field

// In the output capture tasks
while let Ok(Some(line)) = lines.next_line().await {
    log::debug!("STDOUT: {}", line);
-   output_buffer.write().await.push(format!("STDOUT: {}", line));
+   let mut buffer = output_buffer.write().await;
+   buffer.push_back(format!("STDOUT: {}", line));
+   if buffer.len() > max_buffer_size {
+       buffer.pop_front();
+   }
}

Also applies to: 185-191


291-295: Fix graceful shutdown to actually send SIGTERM before SIGKILL

The current implementation calls child.kill() which sends SIGKILL immediately, making the "graceful shutdown" logic ineffective. The log message incorrectly claims to send SIGTERM.

For proper graceful shutdown, you need to send SIGTERM first, then SIGKILL if the process doesn't terminate. Consider using a crate like nix for proper signal handling or implementing platform-specific signal sending.

// Add nix dependency for proper signal handling
use nix::sys::signal::{self, Signal};
use nix::unistd::Pid;

// In the stop method
- match child.kill().await {
-     Ok(_) => log::info!("Sent SIGTERM to process"),
-     Err(e) => log::warn!("Failed to send SIGTERM: {}", e),
- }
+ if let Some(pid) = child.id() {
+     match signal::kill(Pid::from_raw(pid as i32), Signal::SIGTERM) {
+         Ok(_) => log::info!("Sent SIGTERM to process"),
+         Err(e) => log::warn!("Failed to send SIGTERM: {}", e),
+     }
+ }

Also applies to: 317-324

🧹 Nitpick comments (5)
applications/minotari_mcp_common/src/process_launcher.rs (5)

114-252: Consider extracting complex launch logic into smaller methods

The launch method is quite long and handles multiple responsibilities. Consider extracting subprocess tasks like output capture, immediate crash detection, and health waiting into separate methods for better maintainability.

Example refactoring:

pub async fn launch(&self) -> McpResult<LaunchResult> {
    drop(self.status_tx.send(ProcessLaunchStatus::Starting));
    
    let executable_path = self.resolve_executable_path().await?;
    let mut child = self.spawn_process(&executable_path).await?;
    let pid = child.id();
    
    self.start_output_capture(&mut child).await;
    *self.process.write().await = Some(child);
    
    self.check_immediate_crash(pid).await?;
    self.wait_for_health().await?;
    
    let result = LaunchResult {
        process_id: self.launch_id,
        executable_path,
        pid,
        grpc_endpoint: self.config.health_check_config.grpc_endpoint.clone(),
        status: ProcessLaunchStatus::Running,
    };
    
    drop(self.status_tx.send(ProcessLaunchStatus::Running));
    Ok(result)
}

272-282: Improve executable discovery logic

The current discovery logic is quite simplistic, checking for "wallet" or "node" strings in arguments. This could lead to false positives or incorrect detection.

Consider a more robust approach:

async fn discover_executable(&self) -> McpResult<PathBuf> {
-   // Try to determine executable type from arguments
-   if self.config.args.iter().any(|arg| arg.contains("wallet")) {
-       TariExecutables::find_wallet()
-   } else if self.config.args.iter().any(|arg| arg.contains("node")) {
-       TariExecutables::find_node()
-   } else {
-       // Default to node if we can't determine
-       TariExecutables::find_node()
-   }
+   // Look for specific argument patterns
+   let has_wallet_flag = self.config.args.iter().any(|arg| 
+       arg == "--wallet" || arg == "wallet"
+   );
+   let has_node_flag = self.config.args.iter().any(|arg| 
+       arg == "--node" || arg == "node"
+   );
+   
+   match (has_wallet_flag, has_node_flag) {
+       (true, false) => TariExecutables::find_wallet(),
+       (false, true) | (false, false) => TariExecutables::find_node(),
+       (true, true) => Err(McpError::config_error("Ambiguous executable type: both wallet and node specified")),
+   }
}

438-450: Enhance multiaddr conversion with better validation

The current implementation has basic string manipulation but lacks proper validation of IP addresses and port numbers.

Consider adding validation:

fn convert_to_multiaddr(address: &str) -> String {
    if address.starts_with("/ip4/") {
        // Already in multiaddr format
        address.to_string()
    } else if let Some((ip, port)) = address.split_once(':') {
+       // Validate IP address format
+       if std::net::Ipv4Addr::from_str(ip).is_err() {
+           log::warn!("Invalid IP address format: {}", ip);
+       }
+       // Validate port number
+       if port.parse::<u16>().is_err() {
+           log::warn!("Invalid port number: {}", port);
+       }
        // Convert IP:PORT to /ip4/IP/tcp/PORT
        format!("/ip4/{}/tcp/{}", ip, port)
    } else {
+       // Validate port number
+       if address.parse::<u16>().is_err() {
+           log::warn!("Invalid port number: {}", address);
+       }
        // Assume it's just a port, use localhost
        format!("/ip4/127.0.0.1/tcp/{}", address)
    }
}

473-477: Consider making gRPC allowed methods configurable

The hardcoded list of allowed gRPC methods might need to be customizable for different deployment scenarios or security requirements.

Consider adding this to the launch configuration:

pub struct LaunchConfig {
    // ... existing fields
    pub grpc_allowed_methods: Option<Vec<String>>,
}

// In the launch_node method
let allowed_methods = additional_args.iter()
    .find(|arg| arg.starts_with("--grpc-allowed-methods="))
    .map(|arg| arg.strip_prefix("--grpc-allowed-methods=").unwrap_or(""))
    .unwrap_or("get_version,get_tip_info,get_sync_info,get_network_status,get_peers,get_header_by_hash,get_blocks,get_network_difficulty,get_tokens_in_circulation,get_mempool_stats,get_mempool_transactions,get_new_block_template,get_new_block_template_with_coinbases,submit_transaction,submit_block");

558-594: Add more comprehensive tests

The current tests only verify basic configuration building. Consider adding tests for error conditions, process lifecycle, and edge cases.

Add tests for:

  • Invalid health check configurations
  • Multiaddr conversion edge cases
  • Executable discovery logic
  • Error handling scenarios
#[test]
fn test_multiaddr_conversion() {
    assert_eq!(
        TariProcessLauncher::convert_to_multiaddr("127.0.0.1:8080"),
        "/ip4/127.0.0.1/tcp/8080"
    );
    assert_eq!(
        TariProcessLauncher::convert_to_multiaddr("8080"),
        "/ip4/127.0.0.1/tcp/8080"
    );
    assert_eq!(
        TariProcessLauncher::convert_to_multiaddr("/ip4/127.0.0.1/tcp/8080"),
        "/ip4/127.0.0.1/tcp/8080"
    );
}

#[test]
fn test_build_without_health_check() {
    let result = LaunchConfigBuilder::new().build();
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("Health check configuration is required"));
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f31bd9 and dfb3859.

📒 Files selected for processing (3)
  • applications/minotari_mcp_common/src/process_launcher.rs (1 hunks)
  • applications/minotari_mcp_common/src/process_manager.rs (1 hunks)
  • applications/minotari_mcp_node/src/main.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • applications/minotari_mcp_node/src/main.rs
  • applications/minotari_mcp_common/src/process_manager.rs
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: MCozhusheck
PR: tari-project/tari#7262
File: applications/minotari_node/src/grpc/base_node_grpc_server.rs:533-536
Timestamp: 2025-06-30T06:54:18.170Z
Learning: In the Tari codebase, readiness status reporting is split between two gRPC servers: base_node_grpc_server's GetNetworkState method always reports READY when it can respond (indicating network-level readiness), while readiness_grpc_server handles granular readiness states like database initialization and migration progress via ReadinessStatusHandler.
applications/minotari_mcp_common/src/process_launcher.rs (4)
Learnt from: hansieodendaal
PR: tari-project/tari#7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.
Learnt from: hansieodendaal
PR: tari-project/tari#7280
File: base_layer/core/src/chain_storage/blockchain_database.rs:361-438
Timestamp: 2025-07-04T10:56:46.079Z
Learning: The combination of `tokio::task::spawn(async move {` with `tokio::task::spawn_blocking().await` in the payref rebuild background task works well and shuts down properly with the tokio environment, as confirmed by testing in the Tari codebase.
Learnt from: SWvheerden
PR: tari-project/tari#6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.
Learnt from: SWvheerden
PR: tari-project/tari#6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test (nextnet, nextnet)
  • GitHub Check: test (testnet, esmeralda)
  • GitHub Check: test (mainnet, stagenet)
  • GitHub Check: cargo check with stable
  • GitHub Check: ci
  • GitHub Check: ledger build tests
  • GitHub Check: Cucumber tests / Base Layer
  • GitHub Check: Cucumber tests / FFI
🔇 Additional comments (1)
applications/minotari_mcp_common/src/process_launcher.rs (1)

343-349: Fix potential race condition in is_running method

The method takes a write lock when it only needs to read the process status. This could cause unnecessary blocking and doesn't match the method's read-only semantics.

pub async fn is_running(&self) -> bool {
-   if let Some(child) = self.process.write().await.as_mut() {
+   if let Some(child) = self.process.read().await.as_ref() {
        child.try_wait().map(|status| status.is_none()).unwrap_or(false)
    } else {
        false
    }
}

Wait, this won't work because try_wait() requires a mutable reference. The current implementation is actually correct. Let me reconsider this.

Actually, the current implementation is correct - try_wait() needs a mutable reference to the Child process, so a write lock is required. The method name might be misleading since it suggests a read-only operation, but the underlying system call requires mutability.

Copy link
Copy Markdown
Collaborator

@SWvheerden SWvheerden left a comment

Choose a reason for hiding this comment

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

quick review says this is fine.
This is a standalone app so it wont impact existing stuff

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

♻️ Duplicate comments (8)
applications/minotari_mcp_node/src/tools/mempool_tools.rs (1)

107-109: Input limit validation missing at runtime

The existing review comment correctly identifies that while the JSON schema defines a maximum limit of 1000, the runtime code doesn't enforce this constraint, potentially allowing excessive resource usage.

The fix should clamp the limit value:

-        let limit = params.get("limit").and_then(|v| v.as_u64()).unwrap_or(100);
+        let limit = params.get("limit").and_then(|v| v.as_u64()).unwrap_or(100).min(1000);
applications/minotari_mcp_common/src/process_launcher.rs (5)

34-36: startup_timeout field is configured but never used

The startup_timeout field is set by the builder but never consulted by ProcessLauncher. Either integrate it into the launch/health-wait logic or remove it to avoid confusion.


174-182: Unbounded in-memory log capture

Every stdout/stderr line is pushed onto output_buffer, which is never trimmed. A chatty process can exhaust memory or slow the node. Keep only the last n KB/lines, or flush to disk.

Also applies to: 185-193


292-296: "Graceful" shutdown actually sends SIGKILL

Child::kill() maps to SIGKILL on Unix, yet the log states "Sent SIGTERM". This prevents the process from performing cleanup and makes the two-phase TERM/KILL logic moot. Use nix::sys::signal::kill(pid, SIGTERM) first, then fall back to SIGKILL.

Also applies to: 317-324


496-501: Inconsistent timeout configuration

The TariProcessLauncher::launch_node method sets startup_timeout to 120 seconds and max_wait_time to 90 seconds, but since startup_timeout is unused, this creates confusing configuration. Either use both timeouts appropriately or align them.

Remove the unused startup_timeout or implement its usage:

         let config = LaunchConfigBuilder::new()
             .with_executable(TariExecutables::find_node()?)
             .with_args(args)
-            .with_startup_timeout(Duration::from_secs(120))
             .with_health_check(health_config)
             .build()?;

542-547: Inconsistent timeout configuration

Similar to the node launcher, the wallet launcher has misaligned timeout values (200s startup vs 180s health check max wait), creating confusion since startup_timeout is unused.

Remove the unused startup_timeout:

         let config = LaunchConfigBuilder::new()
             .with_executable(TariExecutables::find_wallet()?)
             .with_args(args)
-            .with_startup_timeout(Duration::from_secs(200))
             .with_health_check(health_config)
             .build()?;
applications/minotari_mcp_node/src/tools/mining_tools.rs (2)

64-77: Fix inconsistent algorithm mapping documentation

The algorithm mapping in the error message doesn't match the actual implementation. The error message claims 0=SHA3X, 1=RANDOMXM, 2=RANDOMXT but the code maps 0 => RANDOMXM, 1 => SHA3X, 2 => RANDOMXT.

-        McpError::invalid_request("algo is required (0=SHA3X, 1=RANDOMXM, 2=RANDOMXT)".to_string())
+        McpError::invalid_request("algo is required (0=RANDOMXM, 1=SHA3X, 2=RANDOMXT)".to_string())

107-110: Avoid hardcoded timestamp placeholders

Setting "timestamp": 0 creates misleading API output that could confuse consumers expecting valid Unix timestamps.

Consider using serde_json::Value::Null to omit the field when the actual timestamp isn't available:

-                    "timestamp": 0, // Timestamp not available in NewBlockHeaderTemplate
+                    "timestamp": serde_json::Value::Null, // Timestamp not available in NewBlockHeaderTemplate
🧹 Nitpick comments (6)
applications/minotari_mcp_node/src/tools/mempool_tools.rs (2)

368-368: Unnecessary clippy allow directive

The #[allow(clippy::cast_possible_truncation)] directive appears to be misplaced or unnecessary, as there are no obvious truncating casts in the immediate vicinity.

Remove the unnecessary directive or move it to the actual location where truncation occurs:

-        #[allow(clippy::cast_possible_truncation)]

381-385: Potential integer overflow in fee calculation

The fee per gram calculation could overflow when converting large u64 values to f64 and back, especially with high transaction volumes.

Consider using checked arithmetic or a more robust calculation:

                 "fee_per_gram_avg": if total_weight > 0 {
-                    (total_fees as f64 / total_weight as f64).round() as u64
+                    total_fees.saturating_div(total_weight as u64)
                 } else {
                     0
                 },
applications/minotari_mcp_common/src/process_launcher.rs (1)

474-478: Long hardcoded gRPC method allow-list could become outdated

The hardcoded list of allowed gRPC methods may become stale as new methods are added or existing ones are modified in the base node. Consider making this configurable or using a more maintainable approach.

Would you like me to propose a configuration-based approach or a more dynamic method discovery mechanism for the gRPC allow-list?

applications/minotari_mcp_node/src/tools/mining_tools.rs (3)

384-385: Remove incorrect dead_code attribute

The #[allow(dead_code)] attribute claims the field is used in execute() method line 337-340, but those lines don't exist in this tool and the field is never actually used.

-    #[allow(dead_code)] // False positive: field is used in execute() method line 337-340
-    grpc_client: BaseNodeGrpcClient<Channel>,
+    _grpc_client: BaseNodeGrpcClient<Channel>,

421-429: Incomplete tool implementation

This tool always returns an error and serves no functional purpose. Consider whether this placeholder adds value.

If this tool won't be implemented soon, consider removing it from the module to avoid API clutter. If keeping as a placeholder, simplify the error message:

-        return Err(McpError::invalid_request(
-            "This tool requires a complete block template and coinbase configuration. Use \
-             get_new_block_template_with_coinbases for most use cases."
-                .to_string(),
-        ));
+        Err(McpError::invalid_request(
+            "Tool not yet implemented. Use get_new_block_template_with_coinbases instead.".to_string(),
+        ))

537-541: Potential division by zero in profitability calculation

While there's a check for miner_data.target_difficulty > 0, the division could still result in infinity or NaN if the difficulty is very small.

Consider using a more robust calculation:

                             "profitability_score": if miner_data.target_difficulty > 0 {
-                                ((miner_data.reward + miner_data.total_fees) as f64 / miner_data.target_difficulty as f64 * 1000000.0).round()
+                                let score = (miner_data.reward + miner_data.total_fees) as f64 / miner_data.target_difficulty as f64 * 1000000.0;
+                                if score.is_finite() { score.round() } else { 0.0 }
                             } else {
                                 0.0
                             },
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dfb3859 and e8f3c4c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • Cargo.toml (2 hunks)
  • README.md (8 hunks)
  • applications/minotari_mcp_common/src/process_launcher.rs (1 hunks)
  • applications/minotari_mcp_node/src/cli.rs (1 hunks)
  • applications/minotari_mcp_node/src/main.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/network_status.rs (1 hunks)
  • applications/minotari_mcp_node/src/tools/blockchain_tools.rs (1 hunks)
  • applications/minotari_mcp_node/src/tools/mempool_tools.rs (1 hunks)
  • applications/minotari_mcp_node/src/tools/mining_tools.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • Cargo.toml
  • applications/minotari_mcp_node/src/main.rs
  • README.md
  • applications/minotari_mcp_node/src/resources/network_status.rs
  • applications/minotari_mcp_node/src/cli.rs
  • applications/minotari_mcp_node/src/tools/blockchain_tools.rs
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: MCozhusheck
PR: tari-project/tari#7262
File: applications/minotari_node/src/grpc/base_node_grpc_server.rs:533-536
Timestamp: 2025-06-30T06:54:18.170Z
Learning: In the Tari codebase, readiness status reporting is split between two gRPC servers: base_node_grpc_server's GetNetworkState method always reports READY when it can respond (indicating network-level readiness), while readiness_grpc_server handles granular readiness states like database initialization and migration progress via ReadinessStatusHandler.
applications/minotari_mcp_node/src/tools/mining_tools.rs (4)

Learnt from: hansieodendaal
PR: #7266
File: base_layer/core/src/chain_storage/blockchain_database.rs:457-461
Timestamp: 2025-06-26T13:18:55.898Z
Learning: The MinedInfo struct in the Tari blockchain database contains two optional fields: input: Option<InputMinedInfo> and output: Option<OutputMinedInfo>. This design allows methods returning MinedInfo to always return a valid struct rather than wrapping it in Option, with "not found" scenarios represented through the internal optional fields.

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: hansieodendaal
PR: #6963
File: common_sqlite/src/error.rs:88-92
Timestamp: 2025-05-23T07:49:57.349Z
Learning: In the StorageError enum in common_sqlite/src/error.rs, the HexError variant should keep the manual From implementation rather than using #[from] attribute, as it stores a String representation of the error rather than the HexError type itself.

Learnt from: hansieodendaal
PR: #7294
File: comms/dht/src/network_discovery/seed_strap.rs:352-456
Timestamp: 2025-07-09T08:33:29.320Z
Learning: In comms/dht/src/network_discovery/seed_strap.rs, the context.connectivity.dial_peer method should fail fast and return an error if a peer cannot be dialed, rather than requiring retry logic for general connection failures.

applications/minotari_mcp_node/src/tools/mempool_tools.rs (2)

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: ksrichard
PR: #7129
File: applications/minotari_node/src/http/handler/sync_utxos_by_block.rs:21-29
Timestamp: 2025-05-29T20:59:32.020Z
Learning: In the Tari codebase, request validation for RPC services is handled at the service layer using the serde_valid crate rather than at the HTTP handler level. For example, SyncUtxosByBlockRequest validates that limit is between 1-5 and page is minimum 0. HTTP handlers should delegate validation to the service layer rather than implementing their own validation.

applications/minotari_mcp_common/src/process_launcher.rs (4)

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: hansieodendaal
PR: #7280
File: base_layer/core/src/chain_storage/blockchain_database.rs:361-438
Timestamp: 2025-07-04T10:56:46.079Z
Learning: The combination of tokio::task::spawn(async move { with tokio::task::spawn_blocking().await in the payref rebuild background task works well and shuts down properly with the tokio environment, as confirmed by testing in the Tari codebase.

Learnt from: SWvheerden
PR: #6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.

Learnt from: SWvheerden
PR: #6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.

🧬 Code Graph Analysis (1)
applications/minotari_mcp_node/src/tools/mining_tools.rs (4)
applications/minotari_mcp_node/src/tools/mempool_tools.rs (16)
  • new (19-21)
  • new (73-75)
  • new (183-185)
  • new (272-274)
  • name (26-28)
  • name (80-82)
  • name (190-192)
  • name (279-281)
  • description (30-32)
  • description (84-86)
  • description (194-196)
  • description (283-285)
  • execute (46-63)
  • execute (107-173)
  • execute (215-262)
  • execute (299-412)
applications/minotari_mcp_node/src/tools/blockchain_tools.rs (28)
  • new (26-28)
  • new (127-129)
  • new (202-204)
  • new (301-303)
  • new (374-376)
  • new (436-438)
  • new (535-537)
  • name (33-35)
  • name (134-136)
  • name (209-211)
  • name (308-310)
  • name (381-383)
  • name (443-445)
  • name (542-544)
  • description (37-39)
  • description (138-140)
  • description (213-215)
  • description (312-314)
  • description (385-387)
  • description (447-449)
  • description (546-548)
  • execute (65-117)
  • execute (159-192)
  • execute (238-291)
  • execute (328-364)
  • execute (401-426)
  • execute (479-525)
  • execute (571-616)
applications/minotari_mcp_node/src/tools/network_tools.rs (20)
  • new (19-21)
  • new (101-103)
  • new (225-227)
  • new (363-365)
  • new (421-423)
  • name (26-28)
  • name (108-110)
  • name (232-234)
  • name (370-372)
  • name (428-430)
  • description (30-32)
  • description (112-114)
  • description (236-238)
  • description (374-376)
  • description (432-434)
  • execute (46-91)
  • execute (128-215)
  • execute (259-353)
  • execute (390-411)
  • execute (449-567)
applications/minotari_mcp_common/src/error.rs (2)
  • invalid_request (61-63)
  • tool_execution_failed (73-75)
🔇 Additional comments (4)
applications/minotari_mcp_node/src/tools/mempool_tools.rs (3)

46-64: Well-structured mempool stats implementation

Clean implementation that properly handles the gRPC request/response cycle with appropriate error handling and response formatting.


132-161: Efficient transaction streaming with proper data extraction

Good implementation of streaming transaction data with proper null-safety checks and structured response formatting. The hex encoding for offset and fee calculation logic are correctly implemented.


394-402: Division by zero risk in fee rate calculation

The p75 weight could be zero, leading to division by zero panic on line 401.

Add proper zero-check to prevent panic:

                     let p75_fee = *fee_distribution.get(fee_distribution.len() * 3 / 4).unwrap_or(&0);
                     let p75_weight = *weight_distribution.get(weight_distribution.len() * 3 / 4).unwrap_or(&1);
-                    if p75_weight > 0 { p75_fee / p75_weight as u64 } else { 25 }
+                    if p75_weight > 0 { 
+                        p75_fee.saturating_div(p75_weight as u64).max(1) 
+                    } else { 
+                        25 
+                    }
⛔ Skipped due to learnings
Learnt from: hansieodendaal
PR: tari-project/tari#6974
File: base_layer/wallet/src/output_manager_service/service.rs:2026-2031
Timestamp: 2025-04-23T05:56:30.985Z
Learning: When setting the fee parameter in `output_to_self` method in the Tari wallet, use an accurate fee estimate instead of `fee_per_gram`. The `input_selection.as_final_fee()` method provides a good initial estimate, and the final fee can be obtained later from `stp.get_fee_amount()`.
applications/minotari_mcp_node/src/tools/mining_tools.rs (1)

518-522: Potential division by zero

The code checks if hash_rate > 0 but still performs division without handling the case where miner_data.target_difficulty could be zero.

Add proper zero checks for both values:

                         // Calculate estimated time to find block
-                        let time_to_block = if hash_rate > 0 {
-                            miner_data.target_difficulty / hash_rate
+                        let time_to_block = if hash_rate > 0 && miner_data.target_difficulty > 0 {
+                            miner_data.target_difficulty / hash_rate
                         } else {
                             0
                         };

Likely an incorrect or invalid review comment.

Comment on lines +92 to +98
let health_monitor = Some(
HealthMonitor::new(
"launched_process".to_string(),
config.health_check_config.grpc_endpoint.clone(),
)
.with_timeout(Duration::from_secs(10)),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Hard-coded timeout overrides configuration

The health monitor is created with a hard-coded 10-second timeout, ignoring the configured health_check_config.max_wait_time. This creates inconsistency between the configuration and actual behavior.

-        let health_monitor = Some(
-            HealthMonitor::new(
-                "launched_process".to_string(),
-                config.health_check_config.grpc_endpoint.clone(),
-            )
-            .with_timeout(Duration::from_secs(10)),
-        );
+        let health_monitor = Some(
+            HealthMonitor::new(
+                "launched_process".to_string(),
+                config.health_check_config.grpc_endpoint.clone(),
+            )
+            .with_timeout(config.health_check_config.max_wait_time),
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let health_monitor = Some(
HealthMonitor::new(
"launched_process".to_string(),
config.health_check_config.grpc_endpoint.clone(),
)
.with_timeout(Duration::from_secs(10)),
);
let health_monitor = Some(
HealthMonitor::new(
"launched_process".to_string(),
config.health_check_config.grpc_endpoint.clone(),
)
.with_timeout(config.health_check_config.max_wait_time),
);
🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/process_launcher.rs around lines 92 to
98, the HealthMonitor is initialized with a hard-coded 10-second timeout instead
of using the configured max_wait_time from health_check_config. Replace the
fixed Duration::from_secs(10) with a Duration derived from
config.health_check_config.max_wait_time to ensure the timeout respects the
configuration settings.

Comment on lines +200 to +201
tokio::time::sleep(Duration::from_millis(100)).await;
if !self.is_running().await {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Race condition in immediate crash detection

A 100ms sleep is insufficient to reliably detect process crashes, especially on slow systems or under load. Consider using a longer delay or monitoring process exit status asynchronously.

-        tokio::time::sleep(Duration::from_millis(100)).await;
+        tokio::time::sleep(Duration::from_millis(500)).await;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tokio::time::sleep(Duration::from_millis(100)).await;
if !self.is_running().await {
tokio::time::sleep(Duration::from_millis(500)).await;
if !self.is_running().await {
🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/process_launcher.rs around lines 200 to
201, the 100ms sleep before checking if the process is running is too short and
can cause race conditions in crash detection. Increase the delay duration to a
longer interval to allow the process enough time to start or crash, or refactor
the code to monitor the process exit status asynchronously instead of relying on
a fixed sleep.

Comment on lines +344 to +350
pub async fn is_running(&self) -> bool {
if let Some(child) = self.process.write().await.as_mut() {
child.try_wait().map(|status| status.is_none()).unwrap_or(false)
} else {
false
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Unnecessary write lock for read-only operation

The is_running() method only needs to check process status but takes a write lock unnecessarily. This could block other operations that need the write lock.

-    pub async fn is_running(&self) -> bool {
-        if let Some(child) = self.process.write().await.as_mut() {
-            child.try_wait().map(|status| status.is_none()).unwrap_or(false)
-        } else {
-            false
-        }
-    }
+    pub async fn is_running(&self) -> bool {
+        if let Some(child) = self.process.read().await.as_ref() {
+            // Note: try_wait() requires &mut self, so we need to upgrade to write lock
+            // This is unavoidable due to tokio::process::Child API design
+            drop(child);
+            if let Some(child) = self.process.write().await.as_mut() {
+                child.try_wait().map(|status| status.is_none()).unwrap_or(false)
+            } else {
+                false
+            }
+        } else {
+            false
+        }
+    }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/process_launcher.rs around lines 344 to
350, the is_running() method currently acquires a write lock on self.process
even though it only reads the process status. Change the lock from a write lock
to a read lock by using self.process.read().await instead of
self.process.write().await to avoid blocking other operations that require write
access.

Comment on lines +440 to +451
fn convert_to_multiaddr(address: &str) -> String {
if address.starts_with("/ip4/") {
// Already in multiaddr format
address.to_string()
} else if let Some((ip, port)) = address.split_once(':') {
// Convert IP:PORT to /ip4/IP/tcp/PORT
format!("/ip4/{}/tcp/{}", ip, port)
} else {
// Assume it's just a port, use localhost
format!("/ip4/127.0.0.1/tcp/{}", address)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Multiaddr conversion logic is incomplete

The function assumes IP:PORT format but doesn't validate the input. Invalid formats (missing colon, non-numeric port) will produce malformed multiaddrs that could cause runtime failures.

     fn convert_to_multiaddr(address: &str) -> String {
         if address.starts_with("/ip4/") {
             // Already in multiaddr format
             address.to_string()
         } else if let Some((ip, port)) = address.split_once(':') {
-            // Convert IP:PORT to /ip4/IP/tcp/PORT
-            format!("/ip4/{}/tcp/{}", ip, port)
+            // Validate port is numeric
+            if port.parse::<u16>().is_ok() {
+                format!("/ip4/{}/tcp/{}", ip, port)
+            } else {
+                // Invalid port, treat as single port number
+                format!("/ip4/127.0.0.1/tcp/{}", address)
+            }
         } else {
-            // Assume it's just a port, use localhost
-            format!("/ip4/127.0.0.1/tcp/{}", address)
+            // Validate it's a numeric port
+            if address.parse::<u16>().is_ok() {
+                format!("/ip4/127.0.0.1/tcp/{}", address)
+            } else {
+                // Invalid format, default to a safe fallback
+                "/ip4/127.0.0.1/tcp/18142".to_string()
+            }
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn convert_to_multiaddr(address: &str) -> String {
if address.starts_with("/ip4/") {
// Already in multiaddr format
address.to_string()
} else if let Some((ip, port)) = address.split_once(':') {
// Convert IP:PORT to /ip4/IP/tcp/PORT
format!("/ip4/{}/tcp/{}", ip, port)
} else {
// Assume it's just a port, use localhost
format!("/ip4/127.0.0.1/tcp/{}", address)
}
}
fn convert_to_multiaddr(address: &str) -> String {
if address.starts_with("/ip4/") {
// Already in multiaddr format
address.to_string()
} else if let Some((ip, port)) = address.split_once(':') {
// Validate port is numeric
if port.parse::<u16>().is_ok() {
format!("/ip4/{}/tcp/{}", ip, port)
} else {
// Invalid port, treat as single port number
format!("/ip4/127.0.0.1/tcp/{}", address)
}
} else {
// Validate it's a numeric port
if address.parse::<u16>().is_ok() {
format!("/ip4/127.0.0.1/tcp/{}", address)
} else {
// Invalid format, default to a safe fallback
"/ip4/127.0.0.1/tcp/18142".to_string()
}
}
}
🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/process_launcher.rs around lines 440 to
451, the convert_to_multiaddr function lacks validation for the IP:PORT input
format, which can lead to malformed multiaddrs if the input is invalid. Update
the function to validate that the IP is a valid IPv4 address and the port is a
numeric value before formatting. If validation fails, handle the error
gracefully, such as returning a Result type or a default/fallback multiaddr, to
prevent runtime failures.

Comment on lines +215 to +228
async fn execute(&self, params: Value) -> McpResult<Value> {
let excess_sig_hex = get_required_string_param(&params, "excess_signature")?;

// Parse excess signature - this would need to be properly structured
// For now, we'll assume it's provided in the correct format
let excess_sig_bytes = hex::decode(&excess_sig_hex)
.map_err(|e| McpError::invalid_request(format!("Invalid hex excess signature: {}", e)))?;

// Create signature object - this is a simplified version
// In reality, we'd need to properly parse the signature components
let signature = Signature {
public_nonce: excess_sig_bytes.clone(),
signature: excess_sig_bytes,
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improper signature construction creates invalid cryptographic object

The signature construction reuses the same bytes for both public_nonce and signature fields, which creates an invalid cryptographic signature that won't work with the transaction state API.

This approach will likely cause the gRPC call to fail or return incorrect results. Consider either:

  1. Properly parsing the excess signature into its components, or
  2. Using a different API that accepts raw signature bytes
-        // Create signature object - this is a simplified version
-        // In reality, we'd need to properly parse the signature components
-        let signature = Signature {
-            public_nonce: excess_sig_bytes.clone(),
-            signature: excess_sig_bytes,
-        };
+        // TODO: Properly parse signature into public_nonce and signature components
+        // This current implementation is invalid and needs to be fixed
+        return Err(McpError::invalid_request(
+            "Signature parsing not yet implemented - need to properly split excess_sig_bytes".to_string()
+        ));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn execute(&self, params: Value) -> McpResult<Value> {
let excess_sig_hex = get_required_string_param(&params, "excess_signature")?;
// Parse excess signature - this would need to be properly structured
// For now, we'll assume it's provided in the correct format
let excess_sig_bytes = hex::decode(&excess_sig_hex)
.map_err(|e| McpError::invalid_request(format!("Invalid hex excess signature: {}", e)))?;
// Create signature object - this is a simplified version
// In reality, we'd need to properly parse the signature components
let signature = Signature {
public_nonce: excess_sig_bytes.clone(),
signature: excess_sig_bytes,
};
async fn execute(&self, params: Value) -> McpResult<Value> {
let excess_sig_hex = get_required_string_param(&params, "excess_signature")?;
// Parse excess signature - this would need to be properly structured
// For now, we'll assume it's provided in the correct format
let excess_sig_bytes = hex::decode(&excess_sig_hex)
.map_err(|e| McpError::invalid_request(format!("Invalid hex excess signature: {}", e)))?;
// TODO: Properly parse signature into public_nonce and signature components
// This current implementation is invalid and needs to be fixed
return Err(McpError::invalid_request(
"Signature parsing not yet implemented - need to properly split excess_sig_bytes".to_string()
));
}
🤖 Prompt for AI Agents
In applications/minotari_mcp_node/src/tools/mempool_tools.rs between lines 215
and 228, the signature is incorrectly constructed by assigning the same byte
array to both public_nonce and signature fields, resulting in an invalid
cryptographic object. To fix this, properly parse the excess signature hex
string into its distinct components for public_nonce and signature according to
the expected signature structure, or alternatively, use an API that accepts the
raw signature bytes directly without splitting them.

Comment on lines +176 to +187
async fn execute(&self, _params: Value) -> McpResult<Value> {
// This is a simplified implementation - in reality, we'd need to properly
// construct the NewBlockTemplate from the parameters
return Err(McpError::invalid_request(
"This tool requires a complete block template structure. Use get_new_block_template first.".to_string(),
));

// TODO: Implement proper template parsing when needed
// let template = parse_block_template(&params)?;
// let request = Request::new(template);
// let response = self.grpc_client.clone().get_new_block(request).await?;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Incomplete tool implementation creates dead code

This tool always returns an error and has unused parameters and commented-out code, making it effectively non-functional.

Either implement the functionality or remove this tool from the module. If keeping as a placeholder, consider:

 async fn execute(&self, _params: Value) -> McpResult<Value> {
-    // This is a simplified implementation - in reality, we'd need to properly
-    // construct the NewBlockTemplate from the parameters
     return Err(McpError::invalid_request(
-        "This tool requires a complete block template structure. Use get_new_block_template first.".to_string(),
+        "Tool not yet implemented. Use get_new_block_template for template retrieval.".to_string(),
     ));
-
-    // TODO: Implement proper template parsing when needed
-    // let template = parse_block_template(&params)?;
-    // let request = Request::new(template);
-    // let response = self.grpc_client.clone().get_new_block(request).await?;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn execute(&self, _params: Value) -> McpResult<Value> {
// This is a simplified implementation - in reality, we'd need to properly
// construct the NewBlockTemplate from the parameters
return Err(McpError::invalid_request(
"This tool requires a complete block template structure. Use get_new_block_template first.".to_string(),
));
// TODO: Implement proper template parsing when needed
// let template = parse_block_template(&params)?;
// let request = Request::new(template);
// let response = self.grpc_client.clone().get_new_block(request).await?;
}
async fn execute(&self, _params: Value) -> McpResult<Value> {
return Err(McpError::invalid_request(
"Tool not yet implemented. Use get_new_block_template for template retrieval.".to_string(),
));
}
🤖 Prompt for AI Agents
In applications/minotari_mcp_node/src/tools/mining_tools.rs around lines 176 to
187, the execute function is incomplete and always returns an error, leaving
unused parameters and commented-out code that create dead code. To fix this,
either fully implement the function to parse the parameters and call the gRPC
client as intended, or remove the function and related tool from the module. If
you want to keep it as a placeholder, clearly mark it as unimplemented and
remove unused parameters and commented code to avoid confusion.

Comment on lines +221 to +277
"type": "number",
"description": "Mining algorithm (0=SHA3X, 1=RANDOMXM, 2=RANDOMXT)"
},
"max_weight": {
"type": "number",
"description": "Maximum block weight (optional, default 19500)",
"minimum": 1
},
"coinbases": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string",
"description": "Coinbase recipient address"
},
"value": {
"type": "number",
"description": "Coinbase value"
},
"stealth_payment": {
"type": "boolean",
"description": "Whether to use stealth payment (optional)"
},
"revealed_value_proof": {
"type": "boolean",
"description": "Whether to include revealed value proof (optional)"
},
"coinbase_extra": {
"type": "string",
"description": "Extra coinbase data (optional)"
}
},
"required": ["address", "value"]
},
"description": "Array of coinbase outputs"
}
},
"required": ["algo", "coinbases"]
})
}

async fn execute(&self, params: Value) -> McpResult<Value> {
let algo = params.get("algo").and_then(|v| v.as_u64()).ok_or_else(|| {
McpError::invalid_request("algo is required (0=SHA3X, 1=RANDOMXM, 2=RANDOMXT)".to_string())
})?;

let max_weight = params.get("max_weight").and_then(|v| v.as_u64()).unwrap_or(19500);

let pow_algo_val = match algo {
0 => PowAlgos::Sha3x.into(), // SHA3X
1 => PowAlgos::Randomxm.into(), // RANDOMXM
2 => PowAlgos::Randomxt.into(), // RANDOMXT
_ => {
return Err(McpError::invalid_request(
"Invalid algo: must be 0 (SHA3X), 1 (RANDOMXM), or 2 (RANDOMXT)".to_string(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Inconsistent algorithm mapping across tools

This tool uses a different algorithm mapping (0 => SHA3X, 1 => RANDOMXM, 2 => RANDOMXT) than the first tool (0 => RANDOMXM, 1 => SHA3X, 2 => RANDOMXT). This creates API inconsistency.

Standardize the algorithm mapping across all tools. Update this tool to match the first tool's mapping:

-        let pow_algo_val = match algo {
-            0 => PowAlgos::Sha3x.into(),    // SHA3X
-            1 => PowAlgos::Randomxm.into(), // RANDOMXM
-            2 => PowAlgos::Randomxt.into(), // RANDOMXT
+        let pow_algo_val = match algo {
+            0 => PowAlgos::Randomxm.into(), // RANDOMXM
+            1 => PowAlgos::Sha3x.into(),    // SHA3X
+            2 => PowAlgos::Randomxt.into(), // RANDOMXT

Also update the error message and input schema description:

-            McpError::invalid_request("algo is required (0=SHA3X, 1=RANDOMXM, 2=RANDOMXT)".to_string())
+            McpError::invalid_request("algo is required (0=RANDOMXM, 1=SHA3X, 2=RANDOMXT)".to_string())
-                    "description": "Mining algorithm (0=SHA3X, 1=RANDOMXM, 2=RANDOMXT)"
+                    "description": "Mining algorithm (0=RANDOMXM, 1=SHA3X, 2=RANDOMXT)"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"type": "number",
"description": "Mining algorithm (0=SHA3X, 1=RANDOMXM, 2=RANDOMXT)"
},
"max_weight": {
"type": "number",
"description": "Maximum block weight (optional, default 19500)",
"minimum": 1
},
"coinbases": {
"type": "array",
"items": {
"type": "object",
"properties": {
"address": {
"type": "string",
"description": "Coinbase recipient address"
},
"value": {
"type": "number",
"description": "Coinbase value"
},
"stealth_payment": {
"type": "boolean",
"description": "Whether to use stealth payment (optional)"
},
"revealed_value_proof": {
"type": "boolean",
"description": "Whether to include revealed value proof (optional)"
},
"coinbase_extra": {
"type": "string",
"description": "Extra coinbase data (optional)"
}
},
"required": ["address", "value"]
},
"description": "Array of coinbase outputs"
}
},
"required": ["algo", "coinbases"]
})
}
async fn execute(&self, params: Value) -> McpResult<Value> {
let algo = params.get("algo").and_then(|v| v.as_u64()).ok_or_else(|| {
McpError::invalid_request("algo is required (0=SHA3X, 1=RANDOMXM, 2=RANDOMXT)".to_string())
})?;
let max_weight = params.get("max_weight").and_then(|v| v.as_u64()).unwrap_or(19500);
let pow_algo_val = match algo {
0 => PowAlgos::Sha3x.into(), // SHA3X
1 => PowAlgos::Randomxm.into(), // RANDOMXM
2 => PowAlgos::Randomxt.into(), // RANDOMXT
_ => {
return Err(McpError::invalid_request(
"Invalid algo: must be 0 (SHA3X), 1 (RANDOMXM), or 2 (RANDOMXT)".to_string(),
"type": "number",
"description": "Mining algorithm (0=RANDOMXM, 1=SHA3X, 2=RANDOMXT)"
},
"max_weight": {
"type": "number",
"description": "Maximum block weight (optional, default 19500)",
"minimum": 1
},
// … rest of schema unchanged …
},
"required": ["algo", "coinbases"]
})
}
async fn execute(&self, params: Value) -> McpResult<Value> {
let algo = params.get("algo").and_then(|v| v.as_u64()).ok_or_else(|| {
McpError::invalid_request("algo is required (0=RANDOMXM, 1=SHA3X, 2=RANDOMXT)".to_string())
})?;
let max_weight = params.get("max_weight").and_then(|v| v.as_u64()).unwrap_or(19500);
let pow_algo_val = match algo {
0 => PowAlgos::Randomxm.into(), // RANDOMXM
1 => PowAlgos::Sha3x.into(), // SHA3X
2 => PowAlgos::Randomxt.into(), // RANDOMXT
_ => {
return Err(McpError::invalid_request(
"Invalid algo: must be 0 (RANDOMXM), 1 (SHA3X), or 2 (RANDOMXT)".to_string(),
));
}
};
// … rest of execute unchanged …
🤖 Prompt for AI Agents
In applications/minotari_mcp_node/src/tools/mining_tools.rs between lines 221
and 277, the algorithm mapping for "algo" is inconsistent with the first tool,
currently mapping 0 to SHA3X, 1 to RANDOMXM, and 2 to RANDOMXT. Update the
mapping to match the first tool by mapping 0 to RANDOMXM, 1 to SHA3X, and 2 to
RANDOMXT. Also revise the error message and the input schema description to
reflect this corrected mapping for consistency across tools.

Comment on lines +489 to +494
let pow_algo_val = match algo {
0 => PowAlgos::Sha3x.into(), // SHA3X
1 => PowAlgos::Randomxm.into(), // RANDOMXM
2 => PowAlgos::Randomxt.into(), // RANDOMXT
_ => continue,
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Inconsistent algorithm mapping in analysis tool

This tool uses yet another mapping (0 => SHA3X, 1 => RANDOMXM, 2 => RANDOMXT) that differs from the first tool, creating further API inconsistency.

Update this tool to use the same mapping as the first tool for consistency:

             let pow_algo_val = match algo {
-                0 => PowAlgos::Sha3x.into(),    // SHA3X
-                1 => PowAlgos::Randomxm.into(), // RANDOMXM
+                0 => PowAlgos::Randomxm.into(), // RANDOMXM
+                1 => PowAlgos::Sha3x.into(),    // SHA3X
                 2 => PowAlgos::Randomxt.into(), // RANDOMXT
                 _ => continue,
             };

Also update the input schema and algorithm name mappings for consistency.

🤖 Prompt for AI Agents
In applications/minotari_mcp_node/src/tools/mining_tools.rs around lines 489 to
494, the algorithm mapping used in the match statement is inconsistent with the
first tool, causing API inconsistency. Update the match arms to use the same
algorithm-to-value mapping as the first tool, ensuring the numeric values
correspond to the correct PowAlgos variants. Additionally, revise the input
schema and any algorithm name mappings in this file to align with the first
tool's definitions for consistency across the codebase.

Comment on lines +510 to +515
let algo_name = match algo {
0 => "SHA3X",
1 => "MONERO",
2 => "TARI",
_ => "UNKNOWN",
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Inconsistent algorithm name mapping

The algorithm names used here (SHA3X, MONERO, TARI) don't match the names used elsewhere in the file (SHA3X, RANDOMX_M, RANDOMX_T).

Standardize algorithm names across all tools. Use the same naming convention:

                         let algo_name = match algo {
                             0 => "SHA3X",
-                            1 => "MONERO",
-                            2 => "TARI",
+                            1 => "RANDOMX_M", 
+                            2 => "RANDOMX_T",
                             _ => "UNKNOWN",
                         };

And update the response mapping at lines 592-597 to match:

                 "algo_name": match requested_algo {
                     0 => "SHA3X",
-                    1 => "MONERO",
-                    2 => "TARI",
+                    1 => "RANDOMX_M",
+                    2 => "RANDOMX_T",
                     _ => "UNKNOWN",
                 }
🤖 Prompt for AI Agents
In applications/minotari_mcp_node/src/tools/mining_tools.rs around lines 510 to
515, the algorithm name mapping uses inconsistent names ("SHA3X", "MONERO",
"TARI") compared to other parts of the file ("SHA3X", "RANDOMX_M", "RANDOMX_T").
Update the match arms to use the standardized names "SHA3X", "RANDOMX_M", and
"RANDOMX_T" respectively. Also, ensure that the response mapping at lines
592-597 is updated to use these same standardized names for consistency.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (32)
applications/minotari_mcp_common/src/executable_finder.rs (1)

214-240: Avoid executing untrusted binaries during verification

Running an unknown executable just to call --version can be abused if a malicious binary named minotari_node is present earlier in the search path.

Options:

  1. Skip execution; rely on file name/path & optional checksum.
  2. Sandbox the call (e.g. spawn with seccomp/create_process low-priv).
  3. Make verification opt-in behind an env-flag.

At minimum, document the security implications in the function comment.

applications/minotari_mcp_common/src/security.rs (2)

180-201: Session stats reset on every request

create_or_update_session always creates a fresh Session with request_count = 1, even when a session already exists, discarding history and preventing accurate auditing.

Quick fix:

-let session = Session {
-    id: session_id,
-    client_ip,
-    created_at: now,
-    last_activity: now,
-    request_count: 1,
-};
-self.sessions.insert(session_id.to_string(), session);
+let entry = self.sessions.entry(session_id.to_string()).or_insert(Session{
+    id: session_id,
+    client_ip,
+    created_at: now,
+    last_activity: now,
+    request_count: 0,
+});
+entry.last_activity = now;
+entry.request_count += 1;

237-243: audit_logger.enabled ignores mcp_audit_logging flag

enabled is true only if a path is supplied, silently disabling audit logging when the CLI sets --mcp-audit-logging without a file path. Consider binding enabled to a boolean flag and using the optional path solely for file write-out.

applications/minotari_mcp_common/src/server.rs (3)

168-177: stop never signals the transport

stop() only flips the flag; the StdioTransport keeps running and the task remains alive.
Store a shutdown handle from the transport or expose a cancellation channel so stop() truly terminates the server.


179-183: is_running returns the wrong value

The method always mirrors config.should_start_server() instead of the live running flag, so callers cannot know if the server really started/stopped.

Consider:

-    fn is_running(&self) -> bool {
-        self.config.should_start_server()
-    }
+    fn is_running(&self) -> bool {
+        // Use an atomic or blocking read; simplest fix:
+        futures::executor::block_on(self.running.read()).clone()
+    }

Or swap RwLock<bool> for an AtomicBool to avoid blocking.


250-281: Compile error – sanitized_arguments moved twice

sanitized_arguments is moved into the json! macro (lines 263-266) and then moved again when passed to execute_tool (lines 278-280).
The second move will not compile.

-        let request_data = json!({
-            "tool": params.name,
-            "arguments": sanitized_arguments
-        });
+        let request_data = json!({
+            "tool": params.name,
+            "arguments": sanitized_arguments.clone()
+        });
applications/minotari_mcp_common/src/resources.rs (1)

160-164: Templated-URI pattern is computed incorrectly

extract_pattern turns block/{height} into block/*height, so later matches_pattern() will never succeed.
Replace every {…} segment with a single *.

-fn extract_pattern(&self, uri: &str) -> String {
-    uri.replace('{', "*").replace('}', "")
-}
+fn extract_pattern(&self, uri: &str) -> String {
+    uri.split('/')
+        .map(|part| if part.starts_with('{') && part.ends_with('}') { "*" } else { part })
+        .collect::<Vec<_>>()
+        .join("/")
+}
applications/minotari_mcp_common/src/protobuf_reflector.rs (1)

306-343: Compilation failure – undefined field

apply_field_validation is declared with parameter _field, but the body references field repeatedly, producing an unresolved identifier error.

-    fn apply_field_validation(&self, _field: &FieldDescriptor, schema: &mut Schema) {
+    fn apply_field_validation(&self, field: &FieldDescriptor, schema: &mut Schema) {
applications/minotari_mcp_common/src/startup_diagnostics.rs (1)

522-545: *which is nix-only – prefer cargo --version or command -v

Running which will fail on Windows, incorrectly flagging the build environment.
Replace with a cross-platform check:

-        match Command::new("which").arg("cargo").output() {
+        match Command::new("cargo").arg("--version").output() {
applications/minotari_mcp_common/src/transport.rs (2)

110-118: Include the mandatory jsonrpc:"2.0" field and echo back the original id

JSON-RPC 2.0 responses MUST contain {"jsonrpc":"2.0","id":<same-as-request>, ...}.
Currently the struct omits the field and, on error paths, hard-codes id:null, breaking client correlation and the spec.

 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct McpResponse {
+    #[serde(default = "default_jsonrpc")]
+    pub jsonrpc: String,
     pub id: Value,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub result: Option<Value>,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub error: Option<McpErrorResponse>,
 }
+
+fn default_jsonrpc() -> String {
+    "2.0".to_string()
+}

And preserve the request ID in error responses:

-                                id: Value::Null,
+                                id: message.id().cloned().unwrap_or(Value::Null),

Also applies to: 230-243


214-246: Extract request ID before handling to properly echo it in error responses

The current implementation loses access to the request ID when handling fails, resulting in id: null in error responses.

                     let response = match serde_json::from_str::<McpMessage>(line) {
                         Ok(message) => {
+                            // Extract ID for error responses
+                            let request_id = match &message {
+                                McpMessage::ListTools { id } |
+                                McpMessage::CallTool { id, .. } |
+                                McpMessage::ListResources { id } |
+                                McpMessage::ReadResource { id, .. } |
+                                McpMessage::ListPrompts { id } |
+                                McpMessage::GetPrompt { id, .. } |
+                                McpMessage::Ping { id } |
+                                McpMessage::Initialize { id, .. } => id.clone(),
+                            };
                             // Handle the message
-                            handler.handle_message(message).await
+                            handler.handle_message(message).await.or_else(|e| {
+                                // Create error response with proper ID
+                                Ok(McpResponse {
+                                    id: request_id,
+                                    result: None,
+                                    error: Some(McpErrorResponse {
+                                        code: match &e {
+                                            McpError::PermissionDenied(_) => -32000,
+                                            McpError::InvalidRequest(_) => -32600,
+                                            McpError::ToolNotFound(_) | McpError::ResourceNotFound(_) => -32601,
+                                            _ => -32603,
+                                        },
+                                        message: e.to_string(),
+                                        data: None,
+                                    }),
+                                })
+                            })
                         },
applications/minotari_mcp_common/src/process_manager.rs (1)

74-78: _shutdown_tx is dropped – supervisor stops instantly

Because the sender is discarded, shutdown_rx.recv().await returns None immediately, so the supervisor reports Running and then exits.
Return the sender from new (or store it) so callers can signal shutdown.

-        let (_shutdown_tx, shutdown_rx) = mpsc::unbounded_channel();
+        let (shutdown_tx, shutdown_rx) = mpsc::unbounded_channel();

         Ok((
             Self {
                 process_id: Uuid::new_v4(),
                 process_type,
                 executable_path,
                 args,
                 port,
                 child: RwLock::new(None),
                 status_tx,
+                shutdown_tx: shutdown_tx.clone(),
                 shutdown_rx: RwLock::new(Some(shutdown_rx)),
                 max_restart_attempts: 3,
                 restart_delay_secs: 5,
             },
             status_rx,
+            shutdown_tx,
         ))

Also update the struct definition to include the shutdown sender:

 pub struct ProcessSupervisor {
     process_id: Uuid,
     process_type: ProcessType,
     executable_path: String,
     args: Vec<String>,
     port: u16,
     child: RwLock<Option<Child>>,
     status_tx: mpsc::UnboundedSender<ProcessStatus>,
+    shutdown_tx: mpsc::UnboundedSender<()>,
     shutdown_rx: RwLock<Option<mpsc::UnboundedReceiver<()>>>,
     max_restart_attempts: u32,
     restart_delay_secs: u64,
 }

Also applies to: 81-108

applications/minotari_mcp_common/src/grpc_error_mapper.rs (1)

33-33: Incorrect import path for McpError.

The import path use crate::McpError; is incorrect. Based on the crate structure, McpError is defined in the error module.

-use crate::McpError;
+use crate::error::McpError;
applications/minotari_mcp_common/src/health_monitor.rs (1)

97-104: Use proper URL parsing instead of string splitting.

The current string splitting approach is fragile and won't handle all URL formats correctly (e.g., URLs with authentication, IPv6 addresses, etc.).

-        // Extract host and port from endpoint
-        let endpoint_parts: Vec<&str> = self.endpoint.splitn(2, "://").collect();
-        let address = if endpoint_parts.len() == 2 {
-            endpoint_parts[1] // Remove scheme (http://, https://)
-        } else {
-            &self.endpoint
-        };
+        // Parse the endpoint URL properly
+        let address = if let Ok(url) = url::Url::parse(&self.endpoint) {
+            match (url.host_str(), url.port_or_known_default()) {
+                (Some(host), Some(port)) => format!("{}:{}", host, port),
+                _ => return HealthCheckResult {
+                    status: HealthStatus::Unhealthy,
+                    last_check: Instant::now(),
+                    response_time: None,
+                    error_message: Some("Invalid endpoint URL format".to_string()),
+                    consecutive_failures: 1,
+                }
+            }
+        } else {
+            // Assume it's already in host:port format
+            self.endpoint.clone()
+        };

You'll need to add url as a dependency in Cargo.toml.

applications/minotari_mcp_common/src/grpc_client_implementations.rs (4)

81-91: Use the converted protobuf request in method implementations.

The _proto_request variable is created but never used. The conversion logic should be utilized to pass the properly typed protobuf message to the actual gRPC methods.

For example, in the GetBlocks case, you should use the converted request:

 "GetBlocks" => {
-    // For GetBlocks, we'll use the converted parameters in a real implementation
-    // For now, delegate to get_tip_info as placeholder
-    self.get_tip_info().await
+    let proto_request = self.conversion_registry.convert(method_name, parameters)?;
+    // TODO: Implement actual GetBlocks call using proto_request
+    self.get_blocks_placeholder().await
 },

72-74: Replace panic with error return in deprecated method.

Panicking is too harsh for deprecation. The deprecated method should return an appropriate error instead of panicking so callers can handle it gracefully.

 #[deprecated(note = "Use new() with real client")]
 pub fn new_placeholder() -> Self {
-    // This will panic if called - placeholder is deprecated
-    panic!("Placeholder client deprecated - use real client")
+    // Return a dummy client that will fail on use
+    Self {
+        client: BaseNodeGrpcClient::new(Channel::from_static("http://[::]:1").connect_lazy()),
+        conversion_registry: Arc::new(ConversionRegistry::new()),
+    }
 }

247-248: Use the converted protobuf request for wallet methods.

Same issue as the node client - the conversion result is not being used.


238-241: Replace panic with error return in wallet deprecated method.

Same issue as the node client - panicking is too harsh for deprecation.

 #[deprecated(note = "Use new() with real client")]
 pub fn new_placeholder() -> Self {
-    // This will panic if called - placeholder is deprecated
-    panic!("Placeholder wallet client deprecated - use real client")
+    // Return a dummy client that will fail on use
+    Self {
+        _client: TariWalletGrpcClient::new(Channel::from_static("http://[::]:1").connect_lazy()),
+        conversion_registry: Arc::new(ConversionRegistry::new()),
+    }
 }
applications/minotari_mcp_common/src/schema_generator.rs (1)

287-310: Loss of precision for 64-bit integers

value.as_f64() converts JSON numbers to f64 before min/max checks; values larger than 2^53 lose precision and comparisons become wrong.
Parse via as_u64()/as_i64() first and fall back to f64 only when necessary.

applications/minotari_mcp_common/src/cli_integration.rs (1)

306-311: Same redundancy issue as NodeArgumentBuilder

The log config handling has the same redundancy issue - it could be moved inside the main include_base_args block at line 294 for consistency and clarity.

applications/minotari_mcp_common/src/auto_registry.rs (2)

262-270: ToolStatus statistics never updated in tool execution

As noted in previous reviews, the ToolStatus fields (usage_count, error_count, last_used) are initialized here but DynamicGrpcTool::execute doesn't call record_usage() to update these statistics, making them meaningless.


636-678: Missing usage statistics recording in execute method

The execute method doesn't call AutoDiscoveryRegistry::record_usage() to update tool statistics, making the usage tracking infrastructure ineffective. The method should record both successful and failed executions.

However, there's an architectural issue: DynamicGrpcTool doesn't have a reference to the AutoDiscoveryRegistry to call record_usage(). Consider adding a callback or weak reference to enable usage recording.

 // In DynamicGrpcTool struct, add:
+use_recorder: Option<Arc<dyn Fn(&str, bool, Option<String>) + Send + Sync>>,

 // In execute method, after determining success/failure:
+if let Some(ref recorder) = self.usage_recorder {
+    recorder(&self.metadata.name, result.is_ok(), 
+        result.as_ref().err().map(|e| e.to_string()));
+}
applications/minotari_mcp_common/src/grpc_discovery.rs (1)

149-155: Address the method name mismatch issue in restrict_methods.

As noted in the past review, the restrict_methods function expects full method paths (e.g., "tari.rpc.Wallet/GetBalance"), but callers typically provide only short method names (e.g., "GetBalance"). This mismatch will cause methods to remain unrestricted when they should be restricted.

The filtering logic in available_methods(), control_methods(), and readonly_methods() uses full_name for lookup, but the restriction is likely configured using short names.

Consider normalizing on short method names or accepting both formats:

- pub fn restrict_methods(&mut self, method_names: &[String]) {
-     for name in method_names {
-         self.restricted_methods.insert(name.clone());
-     }
+ pub fn restrict_methods<S: AsRef<str>>(&mut self, method_names: &[S]) {
+     for name in method_names {
+         let name = name.as_ref();
+         // Extract short name from full path or use as-is
+         let short_name = name.split('/').last().unwrap_or(name);
+         self.restricted_methods.insert(short_name.to_lowercase());
+     }
 }

And update the filtering methods to extract short names before lookup.

applications/minotari_mcp_common/src/health_checker.rs (1)

28-32: Use tokio::sync::RwLock instead of std::sync::RwLock in async code.

The module uses std::sync::RwLock but all operations happen in async tasks. Blocking locks can block the entire Tokio worker thread on contention and cause priority inversion.

applications/minotari_mcp_common/src/protobuf_integration.rs (1)

221-225: Fix string type mismatch for McpError::invalid_request.

The call to McpError::invalid_request passes a &str but expects a String.

-return Err(McpError::invalid_request("Parameters must be an object"));
+return Err(McpError::invalid_request("Parameters must be an object".to_string()));
applications/minotari_mcp_common/src/process_launcher.rs (7)

92-98: Hard-coded timeout overrides configuration

The health monitor is created with a hard-coded 10-second timeout, ignoring the configured health_check_config.max_wait_time. This creates inconsistency between the configuration and actual behavior.

-        let health_monitor = Some(
-            HealthMonitor::new(
-                "launched_process".to_string(),
-                config.health_check_config.grpc_endpoint.clone(),
-            )
-            .with_timeout(Duration::from_secs(10)),
-        );
+        let health_monitor = Some(
+            HealthMonitor::new(
+                "launched_process".to_string(),
+                config.health_check_config.grpc_endpoint.clone(),
+            )
+            .with_timeout(config.health_check_config.max_wait_time),
+        );

34-36: startup_timeout is never referenced

LaunchConfig.startup_timeout is set by the builder but never consulted by ProcessLauncher. Either wire it into the launch/health-wait logic or drop the field to avoid misleading configuration knobs.


198-221: Unbounded in-memory log capture

Every stdout/stderr line is pushed onto output_buffer, which is never trimmed. A chatty process can exhaust memory or slow the node. Keep only the last n KB/lines, or flush to disk.


224-225: Race condition in immediate crash detection

A 100ms sleep is insufficient to reliably detect process crashes, especially on slow systems or under load. Consider using a longer delay or monitoring process exit status asynchronously.

-        tokio::time::sleep(Duration::from_millis(100)).await;
+        tokio::time::sleep(Duration::from_millis(500)).await;

316-320: "Graceful" shutdown actually sends SIGKILL

Child::kill() maps to SIGKILL on Unix, yet the log states "Sent SIGTERM". This prevents the process from performing cleanup and makes the two-phase TERM/KILL logic moot (second kill is redundant). Use nix::sys::signal::kill(pid, SIGTERM) (or platform abstraction) first, then fall back to SIGKILL.


368-374: Unnecessary write lock for read-only operation

The is_running() method only needs to check process status but takes a write lock unnecessarily. This could block other operations that need the write lock.


464-475: Multiaddr conversion logic is incomplete

The function assumes IP:PORT format but doesn't validate the input. Invalid formats (missing colon, non-numeric port) will produce malformed multiaddrs that could cause runtime failures.

🧹 Nitpick comments (12)
applications/minotari_mcp_common/src/input_sanitizer.rs (1)

227-229: Consider more restrictive path validation for enhanced security

While allowing /tmp/ paths, consider also validating that paths don't escape the intended working directory using path canonicalization.

         if sanitized_path.starts_with('/') && !sanitized_path.starts_with("/tmp/") {
             return Err(McpError::invalid_request("Absolute paths not allowed (except /tmp/)"));
         }
+        
+        // Validate /tmp/ paths don't escape via symlinks
+        if sanitized_path.starts_with("/tmp/") {
+            if let Ok(canonical) = std::fs::canonicalize(&sanitized_path) {
+                if !canonical.starts_with("/tmp/") {
+                    return Err(McpError::invalid_request("Path escapes /tmp/ via symlinks"));
+                }
+            }
+        }
applications/minotari_mcp_common/src/process_manager.rs (1)

269-280: Implement proper graceful shutdown with SIGTERM before SIGKILL

The current implementation immediately uses kill() which sends SIGKILL, not allowing graceful shutdown. Consider sending SIGTERM first.

On Unix systems, you could use:

#[cfg(unix)]
{
    use nix::sys::signal::{self, Signal};
    use nix::unistd::Pid;
    
    if let Some(pid) = child.id() {
        // Try SIGTERM first for graceful shutdown
        signal::kill(Pid::from_raw(pid as i32), Signal::SIGTERM).ok();
        
        // Wait for graceful shutdown
        match tokio::time::timeout(Duration::from_secs(5), child.wait()).await {
            Ok(Ok(_)) => return Ok(()),
            _ => {
                // Force kill if still running
                child.kill().await.ok();
            }
        }
    }
}
applications/minotari_mcp_common/src/cli_integration.rs (2)

204-210: Minor redundancy in log config handling

The log config section checks include_base_args again, but this check is redundant since it's already within the base arguments section. Consider moving this inside the existing if self.include_base_args block at line 191.

        // Add base arguments only if requested
        if self.include_base_args {
            args.push("--base-path".to_string());
            args.push(self.config.base_path);
            args.push("--config".to_string());
            args.push(self.config.config_path);

            // Add network if specified
            if let Some(network) = self.config.network {
                args.push("--network".to_string());
                args.push(network);
            }
+
+            // Add log config if specified
+            if let Some(log_config) = self.config.log_config {
+                args.push("--log-config".to_string());
+                args.push(log_config.to_string_lossy().to_string());
+            }
        }

-        // Add log config if specified
-        if self.include_base_args {
-            if let Some(log_config) = self.config.log_config {
-                args.push("--log-config".to_string());
-                args.push(log_config.to_string_lossy().to_string());
-            }
-        }

358-360: Potential performance issue with port scanning

The find_available_port function could be slow if base_port is high, as it iterates through all possible ports up to u16::MAX. Consider adding a reasonable upper limit or timeout.

-    pub fn find_available_port(base_port: u16) -> Option<u16> {
-        (base_port..=u16::MAX).find(|&port| std::net::TcpListener::bind(("127.0.0.1", port)).is_ok())
+    pub fn find_available_port(base_port: u16) -> Option<u16> {
+        let max_port = (base_port + 1000).min(u16::MAX);
+        (base_port..=max_port).find(|&port| std::net::TcpListener::bind(("127.0.0.1", port)).is_ok())
+    }
applications/minotari_mcp_common/src/auto_registry.rs (1)

318-318: Parameter extraction not implemented

The TODO comment indicates parameter extraction from schema is not implemented, which means tools won't have proper parameter documentation for users.

Would you like me to help implement the parameter extraction from JSON schema, or should this be tracked as a separate issue?

applications/minotari_mcp_common/src/grpc_discovery.rs (2)

125-131: Consider performance optimization for method filtering.

The available_methods() function performs a linear scan and string comparison for each method. For large numbers of methods, consider using a more efficient lookup structure.

- pub fn available_methods(&self) -> Vec<&GrpcMethodInfo> {
-     self.methods
-         .iter()
-         .filter(|m| !self.restricted_methods.contains(&m.full_name))
-         .collect()
- }

Consider storing method indices or using a different data structure if performance becomes an issue with hundreds of methods.


706-712: Validate JSON schema generation approach.

The schema generation functions return hardcoded serde_json::Value objects. While functional, this approach has maintenance overhead as schemas need manual updates when protobuf definitions change.

Consider whether automatic schema generation from protobuf definitions would be more maintainable in the long term, though the current approach provides explicit control over the schemas exposed to MCP clients.

applications/minotari_mcp_common/src/error.rs (1)

111-121: Improve semantic clarity of connection error constructors.

The connection_failed, service_unavailable, and service_error methods all wrap their messages in ToolExecutionFailed variants, which may confuse error handling logic that checks for specific error types.

Consider either:

  1. Creating dedicated enum variants for these error types, or
  2. Using more specific existing variants like TransportError or ServerError
 pub fn connection_failed(msg: impl Into<String>) -> Self {
-    Self::ToolExecutionFailed(format!("Connection failed: {}", msg.into()))
+    Self::TransportError(format!("Connection failed: {}", msg.into()))
 }

 pub fn service_unavailable(msg: impl Into<String>) -> Self {
-    Self::ToolExecutionFailed(format!("Service unavailable: {}", msg.into()))
+    Self::ServerError(format!("Service unavailable: {}", msg.into()))
 }
applications/minotari_mcp_common/src/health_checker.rs (1)

266-301: Implement proper tonic_health integration.

The connectivity checks are currently placeholders that simulate health checks. Consider implementing actual tonic_health protocol integration for more accurate health monitoring.

// Example of proper tonic_health integration
use tonic_health::pb::health_client::HealthClient;
use tonic_health::pb::HealthCheckRequest;

async fn check_health_protocol(&self, channel: &Channel, service: &str) -> Result<(), Status> {
    let mut client = HealthClient::new(channel.clone());
    let request = HealthCheckRequest {
        service: service.to_string(),
    };
    
    client.check(request).await?;
    Ok(())
}
applications/minotari_mcp_common/src/protobuf_integration.rs (1)

188-218: Complete placeholder implementations.

Several methods return empty placeholders. Consider implementing these or creating tracking issues for future development.

Do you want me to generate implementation stubs for these methods or open tracking issues for:

  • extract_validation_rules
  • generate_parameter_examples
  • document_parameters
applications/minotari_mcp_common/src/lib.rs (2)

58-58: Document reasoning for disabled module

The commented module protobuf_integration mentions "API compatibility fixes" but lacks detail about the specific issues or timeline for resolution.

-// pub mod protobuf_integration; // Disabled pending API compatibility fixes
+// TODO: Re-enable protobuf_integration module once API compatibility issues with 
+// protobuf reflection are resolved (tracked in issue #XXXX)
+// pub mod protobuf_integration;

150-151: Inconsistent commenting style for disabled exports

The commented exports use inconsistent formatting and should align with the module comment style for clarity.

-// Export all macros for public use
-// pub use protobuf_integration::{ReflectiveAutoDiscovery, EnhancedToolMetadata, ToolDocumentation, OpenApiSpec}; //
-// Disabled pending API compatibility fixes
+// TODO: Re-enable protobuf_integration exports once module is restored
+// pub use protobuf_integration::{ReflectiveAutoDiscovery, EnhancedToolMetadata, ToolDocumentation, OpenApiSpec};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e8cbe33 and e0750e5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (35)
  • applications/minotari_mcp_common/src/auto_registry.rs (1 hunks)
  • applications/minotari_mcp_common/src/cli_integration.rs (1 hunks)
  • applications/minotari_mcp_common/src/config.rs (1 hunks)
  • applications/minotari_mcp_common/src/connection_manager.rs (1 hunks)
  • applications/minotari_mcp_common/src/conversion_registry_factory.rs (1 hunks)
  • applications/minotari_mcp_common/src/error.rs (1 hunks)
  • applications/minotari_mcp_common/src/executable_finder.rs (1 hunks)
  • applications/minotari_mcp_common/src/grpc_client_implementations.rs (1 hunks)
  • applications/minotari_mcp_common/src/grpc_discovery.rs (1 hunks)
  • applications/minotari_mcp_common/src/grpc_error_mapper.rs (1 hunks)
  • applications/minotari_mcp_common/src/grpc_executor.rs (1 hunks)
  • applications/minotari_mcp_common/src/health_checker.rs (1 hunks)
  • applications/minotari_mcp_common/src/health_monitor.rs (1 hunks)
  • applications/minotari_mcp_common/src/input_sanitizer.rs (1 hunks)
  • applications/minotari_mcp_common/src/lib.rs (1 hunks)
  • applications/minotari_mcp_common/src/method_implementations.rs (1 hunks)
  • applications/minotari_mcp_common/src/parameter_converter.rs (1 hunks)
  • applications/minotari_mcp_common/src/process_launcher.rs (1 hunks)
  • applications/minotari_mcp_common/src/process_manager.rs (1 hunks)
  • applications/minotari_mcp_common/src/prompts.rs (1 hunks)
  • applications/minotari_mcp_common/src/protobuf_integration.rs (1 hunks)
  • applications/minotari_mcp_common/src/protobuf_reflector.rs (1 hunks)
  • applications/minotari_mcp_common/src/protobuf_reflector_simple.rs (1 hunks)
  • applications/minotari_mcp_common/src/resources.rs (1 hunks)
  • applications/minotari_mcp_common/src/response_converter.rs (1 hunks)
  • applications/minotari_mcp_common/src/schema_generator.rs (1 hunks)
  • applications/minotari_mcp_common/src/security.rs (1 hunks)
  • applications/minotari_mcp_common/src/server.rs (1 hunks)
  • applications/minotari_mcp_common/src/startup_diagnostics.rs (1 hunks)
  • applications/minotari_mcp_common/src/stdio_transport.rs (1 hunks)
  • applications/minotari_mcp_common/src/tool_macros.rs (1 hunks)
  • applications/minotari_mcp_common/src/tool_metadata.rs (1 hunks)
  • applications/minotari_mcp_common/src/tools.rs (1 hunks)
  • applications/minotari_mcp_common/src/transport.rs (1 hunks)
  • applications/minotari_mcp_node/Cargo.toml (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • applications/minotari_mcp_common/src/config.rs
🚧 Files skipped from review as they are similar to previous changes (9)
  • applications/minotari_mcp_node/Cargo.toml
  • applications/minotari_mcp_common/src/tool_macros.rs
  • applications/minotari_mcp_common/src/stdio_transport.rs
  • applications/minotari_mcp_common/src/conversion_registry_factory.rs
  • applications/minotari_mcp_common/src/protobuf_reflector_simple.rs
  • applications/minotari_mcp_common/src/tools.rs
  • applications/minotari_mcp_common/src/prompts.rs
  • applications/minotari_mcp_common/src/response_converter.rs
  • applications/minotari_mcp_common/src/tool_metadata.rs
🧰 Additional context used
🧠 Learnings (21)
📓 Common learnings
Learnt from: MCozhusheck
PR: tari-project/tari#7262
File: applications/minotari_node/src/grpc/base_node_grpc_server.rs:533-536
Timestamp: 2025-06-30T06:54:18.170Z
Learning: In the Tari codebase, readiness status reporting is split between two gRPC servers: base_node_grpc_server's GetNetworkState method always reports READY when it can respond (indicating network-level readiness), while readiness_grpc_server handles granular readiness states like database initialization and migration progress via ReadinessStatusHandler.
applications/minotari_mcp_common/src/executable_finder.rs (3)

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: leet4tari
PR: #7206
File: buildtools/docker_rig/tor.Dockerfile:0-0
Timestamp: 2025-06-18T08:09:27.943Z
Learning: In the Tari project's Docker builds (buildtools/docker_rig/tor.Dockerfile), APK package version constraints like tor>${TOR_VERSION} should remain unquoted despite general Alpine Linux documentation suggesting quoting. Adding quotes around the version constraint breaks the Docker build process in their specific build environment.

Learnt from: SWvheerden
PR: #7111
File: integration_tests/build.rs:7-12
Timestamp: 2025-06-02T07:28:49.228Z
Learning: In build scripts (build.rs files), using .unwrap() on environment variables is acceptable and preferred when you want the build to fail if the required environment variables are not set, rather than using fallbacks.

applications/minotari_mcp_common/src/startup_diagnostics.rs (3)

Learnt from: SWvheerden
PR: #7111
File: integration_tests/build.rs:7-12
Timestamp: 2025-06-02T07:28:49.228Z
Learning: In build scripts (build.rs files), using .unwrap() on environment variables is acceptable and preferred when you want the build to fail if the required environment variables are not set, rather than using fallbacks.

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: hansieodendaal
PR: #7189
File: integration_tests/tests/steps/wallet_cli_steps.rs:464-467
Timestamp: 2025-06-07T06:49:32.174Z
Learning: In integration test code, using panic! for setup validation and prerequisite checking is acceptable and often preferred, as it provides immediate feedback when test scenarios are incorrectly configured rather than allowing tests to continue in invalid states.

applications/minotari_mcp_common/src/resources.rs (1)

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

applications/minotari_mcp_common/src/grpc_error_mapper.rs (2)

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: hansieodendaal
PR: #6963
File: common_sqlite/src/error.rs:88-92
Timestamp: 2025-05-23T07:49:57.349Z
Learning: In the StorageError enum in common_sqlite/src/error.rs, the HexError variant should keep the manual From implementation rather than using #[from] attribute, as it stores a String representation of the error rather than the HexError type itself.

applications/minotari_mcp_common/src/transport.rs (3)

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: SolfataraEmit
PR: #6994
File: docs/src/API_GRPC_Explanation.md:236-244
Timestamp: 2025-04-28T11:41:15.722Z
Learning: The minotari_console_wallet reports a fourth field timelocked_balance in the terminal output for the getBalance method, which should be reflected in the documentation for accuracy. The wallet.proto file confirms that timelocked_balance is defined as the fourth field in the GetBalanceResponse message.

Learnt from: ksrichard
PR: #7129
File: applications/minotari_node/src/http/handler/sync_utxos_by_block.rs:21-29
Timestamp: 2025-05-29T20:59:32.020Z
Learning: In the Tari codebase, request validation for RPC services is handled at the service layer using the serde_valid crate rather than at the HTTP handler level. For example, SyncUtxosByBlockRequest validates that limit is between 1-5 and page is minimum 0. HTTP handlers should delegate validation to the service layer rather than implementing their own validation.

applications/minotari_mcp_common/src/server.rs (2)

Learnt from: hansieodendaal
PR: #7280
File: base_layer/core/src/chain_storage/blockchain_database.rs:361-438
Timestamp: 2025-07-04T10:56:46.079Z
Learning: The combination of tokio::task::spawn(async move { with tokio::task::spawn_blocking().await in the payref rebuild background task works well and shuts down properly with the tokio environment, as confirmed by testing in the Tari codebase.

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

applications/minotari_mcp_common/src/process_manager.rs (3)

Learnt from: hansieodendaal
PR: #7280
File: base_layer/core/src/chain_storage/blockchain_database.rs:361-438
Timestamp: 2025-07-04T10:56:46.079Z
Learning: The combination of tokio::task::spawn(async move { with tokio::task::spawn_blocking().await in the payref rebuild background task works well and shuts down properly with the tokio environment, as confirmed by testing in the Tari codebase.

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: hansieodendaal
PR: #7307
File: comms/core/src/connection_manager/peer_connection.rs:356-357
Timestamp: 2025-07-12T03:43:22.545Z
Learning: In base_layer/core/src/base_node/tari_pulse_service/mod.rs, the disconnect_if_unused call uses parameters (0, 2) because the tari_pulse_service creates exactly 2 substreams and 0 RPC sessions during health checks.

applications/minotari_mcp_common/src/input_sanitizer.rs (1)

Learnt from: ksrichard
PR: #7129
File: applications/minotari_node/src/http/handler/sync_utxos_by_block.rs:21-29
Timestamp: 2025-05-29T20:59:32.020Z
Learning: In the Tari codebase, request validation for RPC services is handled at the service layer using the serde_valid crate rather than at the HTTP handler level. For example, SyncUtxosByBlockRequest validates that limit is between 1-5 and page is minimum 0. HTTP handlers should delegate validation to the service layer rather than implementing their own validation.

applications/minotari_mcp_common/src/schema_generator.rs (1)

Learnt from: ksrichard
PR: #7129
File: applications/minotari_node/src/http/handler/sync_utxos_by_block.rs:21-29
Timestamp: 2025-05-29T20:59:32.020Z
Learning: In the Tari codebase, request validation for RPC services is handled at the service layer using the serde_valid crate rather than at the HTTP handler level. For example, SyncUtxosByBlockRequest validates that limit is between 1-5 and page is minimum 0. HTTP handlers should delegate validation to the service layer rather than implementing their own validation.

applications/minotari_mcp_common/src/grpc_discovery.rs (1)

Learnt from: ksrichard
PR: #7129
File: applications/minotari_node/src/http/handler/sync_utxos_by_block.rs:21-29
Timestamp: 2025-05-29T20:59:32.020Z
Learning: In the Tari codebase, request validation for RPC services is handled at the service layer using the serde_valid crate rather than at the HTTP handler level. For example, SyncUtxosByBlockRequest validates that limit is between 1-5 and page is minimum 0. HTTP handlers should delegate validation to the service layer rather than implementing their own validation.

applications/minotari_mcp_common/src/grpc_client_implementations.rs (5)

Learnt from: SWvheerden
PR: #7301
File: clients/rust/base_node_wallet_client/src/client/http.rs:70-106
Timestamp: 2025-07-11T06:07:36.367Z
Learning: In clients/rust/base_node_wallet_client/src/client/http.rs, the HTTP client server selection logic is intentionally designed to try the local API once and then fallback to the seed server permanently without retry mechanisms. This is the preferred behavior - they want to use local API if available, otherwise use seed server, without continuously retrying the local API.

Learnt from: SolfataraEmit
PR: #6994
File: docs/src/API_GRPC_Explanation.md:236-244
Timestamp: 2025-04-28T11:41:15.722Z
Learning: The minotari_console_wallet reports a fourth field timelocked_balance in the terminal output for the getBalance method, which should be reflected in the documentation for accuracy. The wallet.proto file confirms that timelocked_balance is defined as the fourth field in the GetBalanceResponse message.

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: ksrichard
PR: #7129
File: applications/minotari_node/src/http/handler/sync_utxos_by_block.rs:21-29
Timestamp: 2025-05-29T20:59:32.020Z
Learning: In the Tari codebase, request validation for RPC services is handled at the service layer using the serde_valid crate rather than at the HTTP handler level. For example, SyncUtxosByBlockRequest validates that limit is between 1-5 and page is minimum 0. HTTP handlers should delegate validation to the service layer rather than implementing their own validation.

Learnt from: MCozhusheck
PR: #7262
File: applications/minotari_node/src/grpc/base_node_grpc_server.rs:533-536
Timestamp: 2025-06-30T06:54:18.170Z
Learning: In the Tari codebase, readiness status reporting is split between two gRPC servers: base_node_grpc_server's GetNetworkState method always reports READY when it can respond (indicating network-level readiness), while readiness_grpc_server handles granular readiness states like database initialization and migration progress via ReadinessStatusHandler.

applications/minotari_mcp_common/src/cli_integration.rs (2)

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: SWvheerden
PR: #7111
File: integration_tests/build.rs:7-12
Timestamp: 2025-06-02T07:28:49.228Z
Learning: In build scripts (build.rs files), using .unwrap() on environment variables is acceptable and preferred when you want the build to fail if the required environment variables are not set, rather than using fallbacks.

applications/minotari_mcp_common/src/health_monitor.rs (3)

Learnt from: MCozhusheck
PR: #7262
File: applications/minotari_node/src/grpc/base_node_grpc_server.rs:533-536
Timestamp: 2025-06-30T06:54:18.170Z
Learning: In the Tari codebase, readiness status reporting is split between two gRPC servers: base_node_grpc_server's GetNetworkState method always reports READY when it can respond (indicating network-level readiness), while readiness_grpc_server handles granular readiness states like database initialization and migration progress via ReadinessStatusHandler.

Learnt from: hansieodendaal
PR: #7294
File: comms/dht/src/network_discovery/seed_strap.rs:352-456
Timestamp: 2025-07-09T08:33:29.320Z
Learning: In comms/dht/src/network_discovery/seed_strap.rs, the context.connectivity.dial_peer method should fail fast and return an error if a peer cannot be dialed, rather than requiring retry logic for general connection failures.

Learnt from: hansieodendaal
PR: #7307
File: comms/core/src/connection_manager/peer_connection.rs:356-357
Timestamp: 2025-07-12T03:43:22.545Z
Learning: In base_layer/core/src/base_node/tari_pulse_service/mod.rs, the disconnect_if_unused call uses parameters (0, 2) because the tari_pulse_service creates exactly 2 substreams and 0 RPC sessions during health checks.

applications/minotari_mcp_common/src/parameter_converter.rs (1)

Learnt from: ksrichard
PR: #7129
File: applications/minotari_node/src/http/handler/sync_utxos_by_block.rs:21-29
Timestamp: 2025-05-29T20:59:32.020Z
Learning: In the Tari codebase, request validation for RPC services is handled at the service layer using the serde_valid crate rather than at the HTTP handler level. For example, SyncUtxosByBlockRequest validates that limit is between 1-5 and page is minimum 0. HTTP handlers should delegate validation to the service layer rather than implementing their own validation.

applications/minotari_mcp_common/src/error.rs (2)

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: hansieodendaal
PR: #6963
File: common_sqlite/src/error.rs:88-92
Timestamp: 2025-05-23T07:49:57.349Z
Learning: In the StorageError enum in common_sqlite/src/error.rs, the HexError variant should keep the manual From implementation rather than using #[from] attribute, as it stores a String representation of the error rather than the HexError type itself.

applications/minotari_mcp_common/src/protobuf_integration.rs (2)

Learnt from: hansieodendaal
PR: #6963
File: common_sqlite/src/error.rs:88-92
Timestamp: 2025-05-23T07:49:57.349Z
Learning: In the StorageError enum in common_sqlite/src/error.rs, the HexError variant should keep the manual From implementation rather than using #[from] attribute, as it stores a String representation of the error rather than the HexError type itself.

Learnt from: hansieodendaal
PR: #7189
File: integration_tests/tests/steps/wallet_cli_steps.rs:464-467
Timestamp: 2025-06-07T06:49:32.174Z
Learning: In integration test code, using panic! for setup validation and prerequisite checking is acceptable and often preferred, as it provides immediate feedback when test scenarios are incorrectly configured rather than allowing tests to continue in invalid states.

applications/minotari_mcp_common/src/health_checker.rs (5)

Learnt from: hansieodendaal
PR: #7280
File: base_layer/core/src/chain_storage/blockchain_database.rs:361-438
Timestamp: 2025-07-04T10:56:46.079Z
Learning: The combination of tokio::task::spawn(async move { with tokio::task::spawn_blocking().await in the payref rebuild background task works well and shuts down properly with the tokio environment, as confirmed by testing in the Tari codebase.

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: SWvheerden
PR: #6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.

Learnt from: SWvheerden
PR: #6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.

Learnt from: hansieodendaal
PR: #7307
File: comms/core/src/connection_manager/peer_connection.rs:356-357
Timestamp: 2025-07-12T03:43:22.545Z
Learning: In base_layer/core/src/base_node/tari_pulse_service/mod.rs, the disconnect_if_unused call uses parameters (0, 2) because the tari_pulse_service creates exactly 2 substreams and 0 RPC sessions during health checks.

applications/minotari_mcp_common/src/process_launcher.rs (6)

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: hansieodendaal
PR: #7280
File: base_layer/core/src/chain_storage/blockchain_database.rs:361-438
Timestamp: 2025-07-04T10:56:46.079Z
Learning: The combination of tokio::task::spawn(async move { with tokio::task::spawn_blocking().await in the payref rebuild background task works well and shuts down properly with the tokio environment, as confirmed by testing in the Tari codebase.

Learnt from: SWvheerden
PR: #6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.

Learnt from: SWvheerden
PR: #6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.

Learnt from: hansieodendaal
PR: #7294
File: comms/dht/src/network_discovery/seed_strap.rs:721-735
Timestamp: 2025-07-09T08:13:37.206Z
Learning: In comms/dht/src/network_discovery/seed_strap.rs, the 10-second STREAM_ITEM_TIMEOUT and retry logic are intentionally designed to handle service conflicts where other services kill seed peer connections during seedstrap operations. The underlying discovery_peer/dial_peer API timeouts are too lenient for seedstrap use cases, so the more aggressive timeout with retry logic is appropriate and necessary.

Learnt from: hansieodendaal
PR: #7294
File: comms/dht/src/network_discovery/seed_strap.rs:352-456
Timestamp: 2025-07-09T08:33:29.320Z
Learning: In comms/dht/src/network_discovery/seed_strap.rs, the context.connectivity.dial_peer method should fail fast and return an error if a peer cannot be dialed, rather than requiring retry logic for general connection failures.

applications/minotari_mcp_common/src/connection_manager.rs (3)

Learnt from: hansieodendaal
PR: #6921
File: comms/core/src/connectivity/manager.rs:470-476
Timestamp: 2025-04-03T15:16:27.829Z
Learning: In the Tari codebase's connectivity manager, connections with uncertain state (like timed-out disconnects) are deliberately kept in the pool rather than forcibly removed. The connection pool refresh function runs on a regular interval, which handles any inconsistencies between the actual connection state and how it's tracked in the pool.

Learnt from: hansieodendaal
PR: #7307
File: comms/core/src/connection_manager/peer_connection.rs:356-357
Timestamp: 2025-07-12T03:43:22.545Z
Learning: In base_layer/core/src/base_node/tari_pulse_service/mod.rs, the disconnect_if_unused call uses parameters (0, 2) because the tari_pulse_service creates exactly 2 substreams and 0 RPC sessions during health checks.

Learnt from: hansieodendaal
PR: #7307
File: comms/core/src/connection_manager/peer_connection.rs:356-357
Timestamp: 2025-07-12T03:43:22.545Z
Learning: In comms/core/src/connectivity/manager.rs, the disconnect_if_unused call uses parameters (0, 0) intentionally to be conservative and avoid terminating connections that might be in use. This approach is used while troubleshooting inbound connection drop issues to eliminate the disconnect logic as a potential cause.

applications/minotari_mcp_common/src/grpc_executor.rs (1)

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

🧬 Code Graph Analysis (5)
applications/minotari_mcp_common/src/resources.rs (5)
applications/minotari_mcp_node/src/resources/block_info.rs (7)
  • uri (25-27)
  • name (29-31)
  • description (33-35)
  • mime_type (37-39)
  • read (58-64)
  • supports_templating (41-43)
  • resolve_template (45-56)
applications/minotari_mcp_wallet/src/resources/transaction_info.rs (3)
  • uri (41-43)
  • mime_type (53-55)
  • read (97-99)
applications/minotari_mcp_wallet/src/resources/payment_info.rs (3)
  • uri (39-41)
  • mime_type (51-53)
  • read (128-130)
applications/minotari_mcp_common/src/error.rs (2)
  • invalid_request (83-85)
  • resource_not_found (91-93)
applications/minotari_mcp_common/src/server.rs (1)
  • new (79-86)
applications/minotari_mcp_common/src/transport.rs (3)
applications/minotari_mcp_common/src/stdio_transport.rs (3)
  • serde_json (213-213)
  • serde_json (221-221)
  • handle_message (342-348)
applications/minotari_mcp_common/src/server.rs (2)
  • handle_message (200-215)
  • new (79-86)
applications/minotari_mcp_common/src/error.rs (1)
  • invalid_request (83-85)
applications/minotari_mcp_common/src/auto_registry.rs (5)
applications/minotari_mcp_common/src/grpc_discovery.rs (3)
  • base_node_methods (170-411)
  • wallet_methods (415-701)
  • default (163-165)
applications/minotari_mcp_common/src/tool_metadata.rs (9)
  • new (307-314)
  • new (469-492)
  • category (504-507)
  • from (175-190)
  • tags (524-527)
  • description (51-59)
  • description (132-150)
  • description (499-502)
  • default (454-456)
applications/minotari_mcp_common/src/grpc_executor.rs (1)
  • is_healthy (550-552)
applications/minotari_mcp_common/src/grpc_error_mapper.rs (1)
  • default (482-484)
applications/minotari_mcp_common/src/schema_generator.rs (1)
  • default (498-501)
applications/minotari_mcp_common/src/health_monitor.rs (2)
applications/minotari_mcp_common/src/health_checker.rs (5)
  • new (204-211)
  • check_connectivity (267-293)
  • drop (442-453)
  • check_health (175-175)
  • check_health (380-394)
applications/minotari_mcp_common/src/error.rs (1)
  • server_error (107-109)
applications/minotari_mcp_common/src/error.rs (4)
applications/minotari_mcp_common/src/stdio_transport.rs (2)
  • serde_json (213-213)
  • serde_json (221-221)
applications/minotari_mcp_common/src/transport.rs (1)
  • serde_json (215-215)
applications/minotari_mcp_common/src/grpc_client_implementations.rs (1)
  • from (46-48)
applications/minotari_mcp_common/src/parameter_converter.rs (1)
  • from (64-66)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ledger build tests
  • GitHub Check: ci
🔇 Additional comments (60)
applications/minotari_mcp_common/src/executable_finder.rs (2)

67-112: Comprehensive executable discovery strategy

The multi-strategy approach for finding executables is well-designed, with clear fallbacks and informative error messages that help users troubleshoot missing executables.


171-211: Solid executable validation with proper permission checks

The validation includes appropriate existence, file type, and Unix permission checks. The decision to continue on verification failure with a warning is reasonable for robustness.

applications/minotari_mcp_common/src/security.rs (1)

101-178: Well-designed security permission checking

The permission check implementation properly enforces local-only access, applies rate limiting, validates permission levels, and includes comprehensive audit logging for both success and failure cases.

applications/minotari_mcp_common/src/server.rs (1)

78-124: Well-implemented builder pattern

The McpServerBuilder provides a clean fluent interface for constructing the server with proper validation in the build() method.

applications/minotari_mcp_common/src/resources.rs (2)

33-64: Well-designed resource trait with good defaults

The McpResource trait provides a clean async interface with sensible default implementations for templating support. The error handling using McpError types is appropriate.


214-257: Useful macro for creating static resources

The static_resource! macro provides a convenient way to create static JSON resources with proper async trait implementation and clean encapsulation.

applications/minotari_mcp_common/src/method_implementations.rs (3)

64-111: Robust parameter validation in GetBlocksConverter

The implementation properly handles optional parameters, validates array elements, and provides detailed error messages with element indices. The fallback to empty heights for latest blocks is a good design choice.


149-201: Good transition to registry-based converter registration

The move from factory functions to registry-based registration is well-designed. The deprecation warnings clearly guide users to the new approach while maintaining backward compatibility.


209-247: Comprehensive test coverage

The tests properly cover both success and failure scenarios, including parameter validation and edge cases like empty parameters defaulting to empty heights.

applications/minotari_mcp_common/src/input_sanitizer.rs (1)

329-347: Regex compilation approach is appropriate

Using expect() for compiling hardcoded regex patterns is acceptable since these patterns are known to be valid at compile time.

applications/minotari_mcp_common/src/grpc_error_mapper.rs (3)

35-124: Excellent error context design.

The ErrorContext struct and related enums provide comprehensive error classification with user-friendly messaging, severity levels, and actionable recommendations. The color coding system will enhance UI presentation.


125-485: Comprehensive and well-structured gRPC error mapping.

The GrpcErrorMapper provides thorough mapping of all gRPC status codes to detailed error contexts with user-friendly messages, actionable recommendations, and proper documentation links. The error ID generation and metadata extraction enhance debugging capabilities.


487-598: Well-designed utility functions and comprehensive tests.

The error_utils module provides context-specific error handling for wallet and node scenarios with appropriate severity levels and recommendations. The unit tests effectively verify error mapping, MCP error conversion, and response generation.

applications/minotari_mcp_common/src/parameter_converter.rs (4)

35-67: Excellent error handling design.

The ConversionError enum provides comprehensive error variants for all parameter conversion failure modes with detailed context. The From<ConversionError> implementation properly integrates with the MCP error system.


69-141: Clean trait design and effective registry implementation.

The ParameterConverter trait provides a clean async interface for method-specific conversion. The ConversionRegistry effectively manages converters with proper use of tokio::task::block_in_place for async/sync bridging.


143-255: Comprehensive parameter extraction utilities.

The JsonParameterExtractor trait provides excellent helper methods for extracting typed parameters from JSON with detailed error reporting. The implementation covers all common parameter types with both required and optional variants.


257-281: Adequate test coverage for parameter extraction.

The unit tests effectively verify the JsonParameterExtractor functionality, covering both successful parameter extraction and error cases for missing parameters.

applications/minotari_mcp_common/src/health_monitor.rs (3)

36-59: Well-designed health status representation.

The HealthStatus enum provides comprehensive service states, and HealthCheckResult captures detailed metadata including timing, errors, and failure counts for effective health monitoring.


130-234: Robust health checking with proper retry logic.

The gRPC health check implementation and wait_for_healthy method provide robust service monitoring with exponential backoff, appropriate timeouts, and comprehensive logging for debugging.


236-296: Service-specific configuration and adequate testing.

The ServiceHealthMonitors factory provides appropriate service-specific configurations with different timeouts for base node and wallet services. Unit tests effectively verify core functionality including error scenarios.

applications/minotari_mcp_common/src/grpc_client_implementations.rs (1)

113-387: Appropriate placeholder implementations with one real example.

The method implementations provide reasonable placeholder responses for development. The get_tip_info method demonstrates the proper pattern for real gRPC integration that should be followed for other methods as they're implemented.

applications/minotari_mcp_common/src/schema_generator.rs (3)

35-102: Excellent schema infrastructure design.

The SchemaError enum provides comprehensive error variants, and SchemaGenerator offers a clean interface for schema management and validation with proper integration with ServiceDiscovery.


113-216: Comprehensive schema generation with proper conversions.

The MCP tool schema generation and OpenAPI documentation provide excellent conversion from gRPC methods to user-friendly formats with proper metadata inclusion and comprehensive API documentation.


217-680: Thorough validation logic and comprehensive utilities.

The JSON schema validation logic is comprehensive, covering all data types with proper error reporting. The utility functions provide excellent reusable schema components, and the test coverage effectively verifies the core functionality.

applications/minotari_mcp_common/src/cli_integration.rs (5)

29-29: Good use of targeted clippy allow directive

The #![allow(clippy::vec_init_then_push)] is appropriately placed since the builder patterns in this module legitimately use this pattern for clarity and fluent API design.


34-112: Well-designed configuration and builder pattern

The LaunchCliConfig struct and CliConfigBuilder follow good Rust patterns with clear field documentation and sensible defaults. The builder pattern implementation is clean and follows standard conventions.


114-124: Good trait abstraction for CLI configuration

The CliConfigExtractor trait provides a clean abstraction for different CLI implementations to provide launch configurations and extract arguments. The method signatures are well-designed.


378-382: Standard Default implementation

Clean and appropriate Default implementation for the builder.


384-464: Comprehensive test coverage

Excellent test coverage for all the main functionality including builders, utilities, and validation functions. The test cases are well-structured and cover both positive and negative scenarios.

applications/minotari_mcp_common/src/auto_registry.rs (5)

23-44: Well-documented module with appropriate imports

Good module documentation explaining the auto-discovery system purpose. The imports are comprehensive and well-organized.


46-138: Well-designed configuration structures

The configuration structs and enums are thoughtfully designed with comprehensive options for auto-discovery behavior, server types, tool overrides, and status tracking. The ServerType::name() method provides clean string representation.


139-211: Solid initialization and constructor design

The dual constructor pattern (with/without executor) provides good flexibility. The initialization method has a clear flow: discover → filter → generate → log. Error handling for failed tool generation is appropriate with warning logs.


476-576: Well-implemented registry query methods

The query methods provide comprehensive access to tools with proper health filtering and statistics aggregation. The get_healthy_tools method has robust health checking logic.


681-735: Reasonable defaults and basic test coverage

The AutoDiscoveryConfig::default() provides safe defaults (control disabled). The tests cover core functionality including the risk level determination heuristics. Test coverage could be expanded but covers essential functionality.

applications/minotari_mcp_common/src/grpc_discovery.rs (1)

2035-2036: Regex patterns for hex values are safe as implemented

These patterns are properly anchored (^…$), use a simple character class with a single quantifier, and do not introduce nested or ambiguous quantifiers—so they correctly validate hex‐encoded strings without risk of catastrophic backtracking or unexpected matches. No changes required here.

applications/minotari_mcp_common/src/error.rs (3)

134-148: Validate JSON-RPC error code mappings.

The error code mapping follows JSON-RPC 2.0 specification, but several different error types map to the same code (-32603). This is compliant but may reduce diagnostic precision.

The mapping correctly follows JSON-RPC 2.0 standards:

  • -32700: Parse error (serialization)
  • -32600: Invalid Request
  • -32601: Method not found
  • -32603: Internal error (covers most application errors)

This is a reasonable approach for MCP error reporting.


150-205: Excellent JSON-RPC error formatting with helpful context.

The to_json_rpc_error method provides rich error information including timestamps, error types, and context-specific help messages. This will greatly improve the debugging experience for MCP clients.

The structured error responses with helpful guidance (e.g., "Use 'tools/list' to see available tools") follow good API design practices and will improve developer experience.


69-69: Review automatic From implementations.

The #[from] attributes for serde_json::Error, std::io::Error, and anyhow::Error provide convenient error conversion, which aligns with the manual From implementations seen in the relevant code snippets.

This is consistent with the Tari codebase pattern for error handling and provides good ergonomics for error propagation.

Also applies to: 72-72, 75-75

applications/minotari_mcp_common/src/health_checker.rs (5)

45-73: Well-designed health status enum.

The enum provides appropriate states for service health lifecycle with proper trait implementations.


75-142: Comprehensive health result implementation.

The struct provides detailed metrics tracking and sensible business logic for health assessment. The consecutive failure threshold of 3 for attention alerts is appropriate.


144-188: Well-designed configuration and trait interface.

The default configuration values are sensible and the trait provides a clean async interface for health checking operations.


304-376: Well-structured monitoring task implementation.

The monitoring task properly handles periodic health checks with appropriate logging and state management. The task spawning and interval handling are correctly implemented.


456-514: Comprehensive test coverage.

The tests effectively validate health result operations, configuration defaults, and basic checker functionality with clear assertions.

applications/minotari_mcp_common/src/protobuf_integration.rs (3)

42-90: Well-designed reflective auto-discovery integration.

The struct effectively combines auto-discovery registry with protobuf reflection capabilities, providing enhanced runtime schema generation with appropriate error handling.


135-186: Comprehensive OpenAPI specification generation.

The implementation provides thorough OpenAPI 3.0 spec generation with proper schema mapping and path generation. The structure supports good API documentation.


329-370: Adequate test coverage for current functionality.

The tests effectively validate reflective discovery creation and basic parameter validation behavior with clear assertions.

applications/minotari_mcp_common/src/grpc_executor.rs (6)

60-218: Excellent trait design and constructor patterns.

The NodeGrpcClient and WalletGrpcClient traits provide clean abstractions, and the constructor variants with/without health monitoring offer good flexibility for different deployment scenarios.


219-267: Well-implemented method execution with health awareness.

The implementation properly integrates health checking while maintaining service availability during health warnings. The dispatch logic based on server type is clean and appropriate.


270-323: Robust parameter handling and method dispatch.

The node method execution provides thorough parameter extraction with clear error messages for missing required parameters. The fallback to generic method execution is a good design choice.


325-413: Consistent and thorough wallet method implementation.

The wallet method execution mirrors the quality of the node implementation with proper handling of various parameter types, including arrays for UTXO operations. Error messaging is clear and helpful.


432-553: Comprehensive capability checking and status reporting.

The can_execute method properly matches server types with appropriate method categories. The status reporting provides detailed health and availability information with sensible readiness logic.


555-676: Comprehensive test coverage with integration testing.

The tests include a well-structured mock client, cover executor creation and status reporting, and include integration testing with the conversion registry. The test assertions are clear and thorough.

applications/minotari_mcp_common/src/connection_manager.rs (6)

47-150: Well-designed circuit breaker foundation.

The circuit breaker state management, configuration, and metrics provide a solid foundation following established circuit breaker patterns. The default configuration values are sensible for production use.


153-321: Robust circuit breaker implementation.

The circuit breaker correctly implements state transitions, timeout handling, and comprehensive metrics tracking. The async execution with proper error handling and logging provides good observability.


350-396: Well-implemented connection lifecycle management.

The ManagedConnection provides proper usage tracking, idle detection, and statistics collection. The thread-safe usage counting ensures accurate metrics in concurrent scenarios.


399-504: Excellent integration of connection management components.

The ConnectionManager effectively combines health monitoring, circuit breakers, and connection pooling into a cohesive system. The service addition and connection retrieval logic properly consider health status and circuit breaker states.


506-640: Robust connection pool management with maintenance.

The connection pool creation handles failures gracefully, and the background maintenance task provides effective idle connection cleanup. The statistics collection enables good operational visibility.


642-698: Good test coverage for circuit breaker functionality.

The tests effectively validate circuit breaker states, metrics calculations, and configuration defaults. The commented integration test with network access is appropriately documented for future reference.

applications/minotari_mcp_common/src/lib.rs (2)

27-57: Well-organized module structure

The module organization follows clear domain separation with comprehensive coverage of MCP infrastructure components. The alphabetical ordering enhances maintainability.


60-148: Comprehensive and well-structured public API

The re-exports provide a clean, unified API surface that appropriately exposes the necessary types and traits while maintaining clear boundaries between different functional areas.

Comment on lines +237 to +241
async fn is_process_healthy(&self) -> bool {
// For now, just check if we can connect to the port
// In a full implementation, this would use gRPC health checks
std::net::TcpStream::connect(format!("127.0.0.1:{}", self.port)).is_ok()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use async TCP connection to avoid blocking the executor

The current implementation uses blocking std::net::TcpStream::connect which can block the async executor.

     async fn is_process_healthy(&self) -> bool {
         // For now, just check if we can connect to the port
         // In a full implementation, this would use gRPC health checks
-        std::net::TcpStream::connect(format!("127.0.0.1:{}", self.port)).is_ok()
+        tokio::net::TcpStream::connect(format!("127.0.0.1:{}", self.port))
+            .await
+            .is_ok()
     }

Also update line 315 in is_service_running:

     pub async fn is_service_running(port: u16) -> bool {
-        std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok()
+        tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port))
+            .await
+            .is_ok()
     }
🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/process_manager.rs around lines 237 to
241, replace the blocking std::net::TcpStream::connect call with an asynchronous
TCP connection method, such as tokio::net::TcpStream::connect, to avoid blocking
the async executor. Also, update the is_service_running function at line 315 to
use async TCP connection checks consistently.

Comment on lines +360 to +369
fn test_schema_generation_nonexistent_message() {
// Test with minimal valid descriptor set (would need actual protobuf bytes)
// This is a placeholder for real test data
let descriptor_bytes = vec![]; // Would contain actual FileDescriptorSet bytes

if let Ok(mut reflector) = ProtobufReflector::new(&descriptor_bytes) {
let result = reflector.generate_schema("NonexistentMessage");
assert!(result.is_err());
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add functional tests with real protobuf descriptors

The test uses empty descriptor bytes, making it ineffective at testing schema generation logic. Consider generating actual FileDescriptorSet bytes from a test proto file.

-        let descriptor_bytes = vec![]; // Would contain actual FileDescriptorSet bytes
+        // Generate descriptor bytes from a test proto file or use hardcoded bytes
+        // Example: include_bytes!("../test_data/test.descriptor.bin")
+        let descriptor_bytes = generate_test_descriptor_bytes();

Consider adding a test proto file and compiling it to descriptors as part of the build process, or include pre-compiled descriptor bytes for testing.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/protobuf_reflector.rs around lines 360
to 369, the test uses empty descriptor bytes, which does not effectively test
schema generation. Fix this by adding a real test proto file, compiling it to
generate actual FileDescriptorSet bytes, and using those bytes in the test. This
can be done by including pre-compiled descriptor bytes or integrating proto
compilation into the build process to provide valid descriptor data for the
test.

// Check network configuration and ports
results.extend(self.check_network_configuration().await);

// Check running services
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle channel send errors instead of silently dropping them

Using drop() to ignore send errors could hide important failures. Consider logging when sends fail:

-                drop(self.status_tx.send(ProcessStatus::Running));
+                if let Err(e) = self.status_tx.send(ProcessStatus::Running) {
+                    log::warn!("Failed to send status update: {:?}", e);
+                }

Also applies to: 132-132, 137-137, 149-149, 162-162, 171-171, 177-177, 186-186

🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/startup_diagnostics.rs at lines 125,
132, 137, 149, 162, 171, 177, and 186, the code currently uses drop() to ignore
errors from channel send operations, which can hide failures. Modify the code to
handle these send errors explicitly by checking the result of the send operation
and logging an error message if the send fails, instead of silently dropping the
error.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
applications/minotari_mcp_node/src/prompts/mod.rs (1)

25-32: Remove unused McpResult import to keep the file warning-free.

McpResult is not used anywhere in this file and should be removed to avoid compiler warnings.

Apply this diff to remove the unused import:

 use minotari_mcp_common::{
     prompts::MessageRole,
     resource_message,
     simple_prompt,
     text_message,
-    McpResult,
     PromptRegistry,
 };
🧹 Nitpick comments (2)
applications/minotari_mcp_node/src/resources/chain_metadata.rs (2)

21-24: Fix inconsistent file description.

The file description mentions "Common MCP infrastructure" but this file contains a node-specific resource implementation, not common infrastructure.

Apply this diff to correct the description:

-// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.//! Common MCP (Model Context Protocol)
-// infrastructure for Tari applications
+// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
 //! Chain metadata resource

72-84: Consider simplifying the JSON structure to eliminate duplication.

The current JSON structure duplicates several fields between the root level and the nested chain_metadata object, which adds complexity without clear benefit.

Consider this simplified structure:

 Ok(serde_json::json!({
     "best_block_height": metadata.best_block_height,
     "best_block_hash": hex::encode(&metadata.best_block_hash),
     "accumulated_difficulty": hex::encode(&metadata.accumulated_difficulty),
     "pruned_height": metadata.pruned_height,
-    "timestamp": metadata.timestamp,
-    "chain_metadata": {
-        "best_block_height": metadata.best_block_height,
-        "accumulated_difficulty": hex::encode(&metadata.accumulated_difficulty),
-        "pruned_height": metadata.pruned_height,
-    }
+    "timestamp": metadata.timestamp
 }))
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0750e5 and 561af7c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • Cargo.toml (2 hunks)
  • applications/minotari_mcp_node/src/cli.rs (1 hunks)
  • applications/minotari_mcp_node/src/config.rs (1 hunks)
  • applications/minotari_mcp_node/src/grpc_config_parser.rs (1 hunks)
  • applications/minotari_mcp_node/src/main.rs (1 hunks)
  • applications/minotari_mcp_node/src/prompts/mod.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/block_info.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/chain_metadata.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/mempool_stats.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/mod.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/network_difficulty.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/network_status.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/peer_list.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/sync_progress.rs (1 hunks)
  • applications/minotari_mcp_node/src/resources/transaction_info.rs (1 hunks)
  • applications/minotari_mcp_node/src/server.rs (1 hunks)
  • applications/minotari_mcp_node/src/tools/ban_peer.rs (1 hunks)
  • applications/minotari_mcp_node/src/tools/blockchain_tools.rs (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • applications/minotari_mcp_node/src/resources/transaction_info.rs
  • applications/minotari_mcp_node/src/resources/sync_progress.rs
  • applications/minotari_mcp_node/src/resources/peer_list.rs
🚧 Files skipped from review as they are similar to previous changes (13)
  • Cargo.toml
  • applications/minotari_mcp_node/src/resources/mempool_stats.rs
  • applications/minotari_mcp_node/src/resources/network_difficulty.rs
  • applications/minotari_mcp_node/src/resources/mod.rs
  • applications/minotari_mcp_node/src/resources/network_status.rs
  • applications/minotari_mcp_node/src/main.rs
  • applications/minotari_mcp_node/src/resources/block_info.rs
  • applications/minotari_mcp_node/src/cli.rs
  • applications/minotari_mcp_node/src/config.rs
  • applications/minotari_mcp_node/src/grpc_config_parser.rs
  • applications/minotari_mcp_node/src/tools/blockchain_tools.rs
  • applications/minotari_mcp_node/src/tools/ban_peer.rs
  • applications/minotari_mcp_node/src/server.rs
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: MCozhusheck
PR: tari-project/tari#7262
File: applications/minotari_node/src/grpc/base_node_grpc_server.rs:533-536
Timestamp: 2025-06-30T06:54:18.170Z
Learning: In the Tari codebase, readiness status reporting is split between two gRPC servers: base_node_grpc_server's GetNetworkState method always reports READY when it can respond (indicating network-level readiness), while readiness_grpc_server handles granular readiness states like database initialization and migration progress via ReadinessStatusHandler.
applications/minotari_mcp_node/src/prompts/mod.rs (1)

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: test (testnet, esmeralda)
  • GitHub Check: cargo check with stable
  • GitHub Check: test (mainnet, stagenet)
  • GitHub Check: ci
  • GitHub Check: test (nextnet, nextnet)
  • GitHub Check: ledger build tests
  • GitHub Check: Cucumber tests / Base Layer
  • GitHub Check: Cucumber tests / FFI
🔇 Additional comments (5)
applications/minotari_mcp_node/src/resources/chain_metadata.rs (2)

25-32: LGTM!

The imports are well-organized and all necessary for the implementation.


33-42: LGTM!

Clean struct design with proper use of Arc for shared ownership and a straightforward constructor.

applications/minotari_mcp_node/src/prompts/mod.rs (3)

34-41: LGTM!

Clean factory pattern with appropriate clippy allow attribute and good documentation.


43-147: LGTM!

Excellent prompt definitions that provide clear, structured guidance for AI agents. Each prompt:

  • Has well-defined system and user messages
  • References appropriate resources for context
  • Addresses specific node operational scenarios
  • Follows consistent formatting and structure

The prompts effectively cover the main use cases for node diagnostics and management.


148-150: LGTM!

Clean completion of the factory method.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (10)
applications/minotari_mcp_common/src/process_manager.rs (3)

74-78: Critical shutdown mechanism flaw prevents proper process control.

The _shutdown_tx sender is immediately dropped, causing shutdown_rx.recv().await to return None instantly, making the supervisor exit prematurely without waiting for shutdown signals.


237-241: Replace blocking TCP connection with async equivalent.

The blocking std::net::TcpStream::connect can stall the async executor. Use tokio::net::TcpStream::connect().await instead.


289-316: Well-designed utility functions with one blocking operation issue.

The port management and command building utilities are well-implemented with proper error handling. However, line 315 uses blocking std::net::TcpStream::connect which should be replaced with tokio::net::TcpStream::connect().await for consistency with async patterns.

applications/minotari_mcp_common/src/process_launcher.rs (7)

57-59: Unused configuration field creates misleading interface.

The startup_timeout field in LaunchConfig is never used by ProcessLauncher, making it a misleading configuration option.


115-121: Hard-coded timeout overrides user configuration.

The health monitor uses a fixed 10-second timeout instead of respecting config.health_check_config.max_wait_time, creating inconsistency between configuration and behavior.


196-220: Unbounded output buffer risks memory exhaustion.

The output capture tasks continuously append to output_buffer without size limits, potentially causing memory issues with verbose processes.


222-224: Race condition in immediate crash detection.

The 100ms delay before checking process status may be insufficient to reliably detect crashes, especially on loaded systems.


315-319: Incorrect signal handling prevents graceful shutdown.

Child::kill() sends SIGKILL immediately but the log claims to send SIGTERM, bypassing graceful shutdown opportunity.


367-373: Unnecessary write lock blocks concurrent operations.

The is_running() method uses a write lock when a read lock should suffice for checking process status.


463-474: Missing input validation in multiaddr conversion.

The convert_to_multiaddr function lacks validation for IP:PORT format, potentially creating malformed multiaddrs that could cause runtime failures.

🧹 Nitpick comments (3)
applications/minotari_mcp_common/src/input_sanitizer.rs (3)

47-65: Consider extracting HTML entities into a constant or method.

The HTML entity mapping is duplicated between Default::default() and from_config(), which violates the DRY principle and makes maintenance harder.

+impl InputSanitizer {
+    fn default_html_entities() -> HashMap<String, String> {
+        let mut html_entities = HashMap::new();
+        html_entities.insert("&amp;".to_string(), "&".to_string());
+        html_entities.insert("&lt;".to_string(), "<".to_string());
+        html_entities.insert("&gt;".to_string(), ">".to_string());
+        html_entities.insert("&quot;".to_string(), "\"".to_string());
+        html_entities.insert("&#39;".to_string(), "'".to_string());
+        html_entities.insert("&apos;".to_string(), "'".to_string());
+        html_entities.insert("&#x27;".to_string(), "'".to_string());
+        html_entities.insert("&#x2F;".to_string(), "/".to_string());
+        html_entities.insert("&#47;".to_string(), "/".to_string());
+        html_entities
+    }
+}
+
 impl Default for InputSanitizer {
     fn default() -> Self {
-        let mut html_entities = HashMap::new();
-        html_entities.insert("&amp;".to_string(), "&".to_string());
-        html_entities.insert("&lt;".to_string(), "<".to_string());
-        html_entities.insert("&gt;".to_string(), ">".to_string());
-        html_entities.insert("&quot;".to_string(), "\"".to_string());
-        html_entities.insert("&#39;".to_string(), "'".to_string());
-        html_entities.insert("&apos;".to_string(), "'".to_string());
-        html_entities.insert("&#x27;".to_string(), "'".to_string());
-        html_entities.insert("&#x2F;".to_string(), "/".to_string());
-        html_entities.insert("&#47;".to_string(), "/".to_string());

         Self {
             max_string_length: 65536, // 64KB max string
             max_array_length: 1000,   // Max 1000 array elements
             max_object_depth: 32,     // Max 32 levels of nesting
-            allowed_html_entities: html_entities,
+            allowed_html_entities: Self::default_html_entities(),
         }
     }
 }

79-101: Apply the same refactoring to eliminate code duplication.

The same HTML entity mapping duplication exists in the from_config method.

     pub fn from_config(config: &InputSanitizationConfig) -> Self {
-        let mut html_entities = HashMap::new();
-
-        if config.clean_html_entities {
-            html_entities.insert("&amp;".to_string(), "&".to_string());
-            html_entities.insert("&lt;".to_string(), "<".to_string());
-            html_entities.insert("&gt;".to_string(), ">".to_string());
-            html_entities.insert("&quot;".to_string(), "\"".to_string());
-            html_entities.insert("&#39;".to_string(), "'".to_string());
-            html_entities.insert("&apos;".to_string(), "'".to_string());
-            html_entities.insert("&#x27;".to_string(), "'".to_string());
-            html_entities.insert("&#x2F;".to_string(), "/".to_string());
-            html_entities.insert("&#47;".to_string(), "/".to_string());
-        }
+        let html_entities = if config.clean_html_entities {
+            Self::default_html_entities()
+        } else {
+            HashMap::new()
+        };

         Self {
             max_string_length: config.max_string_length,

329-332: Optimize regex compilation for better performance.

The regex patterns are compiled on every call, which is inefficient. Consider using lazy_static or once_cell for compilation.

+use once_cell::sync::Lazy;
+
+static BASE58_PATTERN: Lazy<regex::Regex> = Lazy::new(|| {
+    regex::Regex::new(r"^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$")
+        .expect("Valid regex pattern")
+});
+
 impl ValidationPatterns {
     /// Base58 pattern for Tari addresses
-    pub fn base58() -> regex::Regex {
-        regex::Regex::new(r"^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$")
-            .expect("Valid regex pattern")
+    pub fn base58() -> &'static regex::Regex {
+        &BASE58_PATTERN
     }

Apply the same pattern to all other regex methods.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 561af7c and 20ce2e4.

⛔ Files ignored due to path filters (4)
  • Cargo.lock is excluded by !**/*.lock
  • applications/minotari_ledger_wallet/wallet/Cargo.lock is excluded by !**/*.lock
  • clients/ffi_client/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • applications/minotari_ledger_wallet/comms/Cargo.toml (1 hunks)
  • applications/minotari_ledger_wallet/wallet/Cargo.toml (1 hunks)
  • applications/minotari_mcp_common/src/input_sanitizer.rs (1 hunks)
  • applications/minotari_mcp_common/src/process_launcher.rs (1 hunks)
  • applications/minotari_mcp_common/src/process_manager.rs (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • applications/minotari_ledger_wallet/comms/Cargo.toml
  • applications/minotari_ledger_wallet/wallet/Cargo.toml
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: MCozhusheck
PR: tari-project/tari#7262
File: applications/minotari_node/src/grpc/base_node_grpc_server.rs:533-536
Timestamp: 2025-06-30T06:54:18.170Z
Learning: In the Tari codebase, readiness status reporting is split between two gRPC servers: base_node_grpc_server's GetNetworkState method always reports READY when it can respond (indicating network-level readiness), while readiness_grpc_server handles granular readiness states like database initialization and migration progress via ReadinessStatusHandler.
applications/minotari_mcp_common/src/process_manager.rs (8)

Learnt from: hansieodendaal
PR: #7280
File: base_layer/core/src/chain_storage/blockchain_database.rs:361-438
Timestamp: 2025-07-04T10:56:46.079Z
Learning: The combination of tokio::task::spawn(async move { with tokio::task::spawn_blocking().await in the payref rebuild background task works well and shuts down properly with the tokio environment, as confirmed by testing in the Tari codebase.

Learnt from: hansieodendaal
PR: #7294
File: comms/dht/src/network_discovery/seed_strap.rs:352-456
Timestamp: 2025-07-09T08:33:29.320Z
Learning: In comms/dht/src/network_discovery/seed_strap.rs, the context.connectivity.dial_peer method should fail fast and return an error if a peer cannot be dialed, rather than requiring retry logic for general connection failures.

Learnt from: hansieodendaal
PR: #7294
File: comms/dht/src/network_discovery/seed_strap.rs:352-456
Timestamp: 2025-07-09T08:33:29.320Z
Learning: In comms/dht/src/network_discovery/seed_strap.rs, the fetch_peers_from_connection and collect_peer_stream functions rely on RPC streaming, and when the main connection is closed by another process, collect_peer_stream times out after STREAM_ITEM_TIMEOUT because it cannot detect that the peer can no longer respond, returning an empty vector of peers. This is why the connection state check is important for the retry logic.

Learnt from: hansieodendaal
PR: #7307
File: comms/core/src/connection_manager/peer_connection.rs:356-357
Timestamp: 2025-07-12T03:43:22.545Z
Learning: In base_layer/core/src/base_node/tari_pulse_service/mod.rs, the disconnect_if_unused call uses parameters (0, 2) because the tari_pulse_service creates exactly 2 substreams and 0 RPC sessions during health checks.

Learnt from: hansieodendaal
PR: #7307
File: comms/core/src/connection_manager/peer_connection.rs:356-357
Timestamp: 2025-07-12T03:43:22.545Z
Learning: In comms/core/src/connectivity/manager.rs, the disconnect_if_unused call uses parameters (0, 0) intentionally to be conservative and avoid terminating connections that might be in use. This approach is used while troubleshooting inbound connection drop issues to eliminate the disconnect logic as a potential cause.

Learnt from: SWvheerden
PR: #6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.

Learnt from: SWvheerden
PR: #6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

applications/minotari_mcp_common/src/input_sanitizer.rs (1)

Learnt from: ksrichard
PR: #7129
File: applications/minotari_node/src/http/handler/sync_utxos_by_block.rs:21-29
Timestamp: 2025-05-29T20:59:32.020Z
Learning: In the Tari codebase, request validation for RPC services is handled at the service layer using the serde_valid crate rather than at the HTTP handler level. For example, SyncUtxosByBlockRequest validates that limit is between 1-5 and page is minimum 0. HTTP handlers should delegate validation to the service layer rather than implementing their own validation.

applications/minotari_mcp_common/src/process_launcher.rs (6)

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

Learnt from: hansieodendaal
PR: #7280
File: base_layer/core/src/chain_storage/blockchain_database.rs:361-438
Timestamp: 2025-07-04T10:56:46.079Z
Learning: The combination of tokio::task::spawn(async move { with tokio::task::spawn_blocking().await in the payref rebuild background task works well and shuts down properly with the tokio environment, as confirmed by testing in the Tari codebase.

Learnt from: SWvheerden
PR: #6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.

Learnt from: SWvheerden
PR: #6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.

Learnt from: hansieodendaal
PR: #7294
File: comms/dht/src/network_discovery/seed_strap.rs:721-735
Timestamp: 2025-07-09T08:13:37.206Z
Learning: In comms/dht/src/network_discovery/seed_strap.rs, the 10-second STREAM_ITEM_TIMEOUT and retry logic are intentionally designed to handle service conflicts where other services kill seed peer connections during seedstrap operations. The underlying discovery_peer/dial_peer API timeouts are too lenient for seedstrap use cases, so the more aggressive timeout with retry logic is appropriate and necessary.

Learnt from: hansieodendaal
PR: #7294
File: comms/dht/src/network_discovery/seed_strap.rs:352-456
Timestamp: 2025-07-09T08:33:29.320Z
Learning: In comms/dht/src/network_discovery/seed_strap.rs, the context.connectivity.dial_peer method should fail fast and return an error if a peer cannot be dialed, rather than requiring retry logic for general connection failures.

🧬 Code Graph Analysis (1)
applications/minotari_mcp_common/src/input_sanitizer.rs (10)
applications/minotari_mcp_common/src/stdio_transport.rs (3)
  • serde_json (213-213)
  • serde_json (221-221)
  • new (49-51)
applications/minotari_mcp_common/src/config.rs (3)
  • default (99-108)
  • default (112-121)
  • default (125-136)
applications/minotari_mcp_common/src/auto_registry.rs (3)
  • default (682-691)
  • new (141-157)
  • new (599-611)
applications/minotari_mcp_common/src/connection_manager.rs (5)
  • default (59-61)
  • default (90-98)
  • default (147-149)
  • default (339-347)
  • new (115-126)
applications/minotari_mcp_common/src/grpc_error_mapper.rs (1)
  • default (482-484)
applications/minotari_mcp_common/src/parameter_converter.rs (1)
  • default (138-140)
applications/minotari_mcp_common/src/schema_generator.rs (3)
  • default (498-501)
  • value (382-382)
  • value (390-390)
applications/minotari_mcp_common/src/process_manager.rs (1)
  • new (69-93)
applications/minotari_mcp_common/src/prompts.rs (1)
  • new (97-101)
applications/minotari_mcp_common/src/error.rs (1)
  • invalid_request (83-85)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: test (mainnet, stagenet)
  • GitHub Check: test (nextnet, nextnet)
  • GitHub Check: test (testnet, esmeralda)
  • GitHub Check: cargo check with stable
  • GitHub Check: ledger build tests
  • GitHub Check: ci
🔇 Additional comments (8)
applications/minotari_mcp_common/src/process_manager.rs (3)

101-188: Well-structured supervision logic with proper restart handling.

The supervision loop correctly implements health checking, restart attempts with backoff, and comprehensive error handling. The logic will work correctly once the shutdown mechanism issue (lines 74-78) is resolved.


191-283: Comprehensive process lifecycle management with proper error handling.

The process launching, monitoring, and shutdown methods demonstrate good practices:

  • Proper error propagation and logging
  • Health check waiting with reasonable timeouts
  • Graceful shutdown with force kill fallback
  • Appropriate child process management

318-421: Excellent command building utilities and appropriate test coverage.

The CLI argument construction functions demonstrate good understanding of Tari application interfaces, with proper handling of optional parameters and required flags. The test suite appropriately covers the core utility functions.

applications/minotari_mcp_common/src/process_launcher.rs (4)

137-276: Well-structured process launching with comprehensive error handling.

Despite the identified issues with output buffering and crash detection timing, the overall launch logic demonstrates good practices:

  • Proper executable discovery
  • Comprehensive command setup with environment and working directory
  • Good error reporting and status updates
  • Integration with health monitoring

390-456: Excellent builder pattern implementation with proper validation.

The LaunchConfigBuilder follows Rust conventions with clear method names, proper chaining, and appropriate validation in the build() method. The required health check configuration validation is particularly good.


476-574: Excellent Tari service launching utilities with appropriate configurations.

The launch_node and launch_wallet methods demonstrate deep understanding of Tari CLI interfaces:

  • Proper gRPC configuration with comprehensive allowed methods for nodes
  • Appropriate health check timeouts (90s for node, 180s for wallet)
  • Good argument construction and parameter handling
  • Proper multiaddr format usage

582-618: Appropriate test coverage for configuration and builder functionality.

The tests properly validate the builder pattern implementation and configuration construction, ensuring that values are correctly assigned and the API works as expected.

applications/minotari_mcp_common/src/input_sanitizer.rs (1)

350-463: LGTM! Comprehensive test coverage.

The test suite covers all major functionality including:

  • HTML entity sanitization
  • Depth limits
  • String/array length limits
  • Path validation (including security cases)
  • URL validation (including malicious inputs)
  • Numeric range validation (including edge cases like NaN/infinity)
  • Pattern validation

The tests properly validate both positive and negative cases, which is excellent for security-critical code.

Comment on lines +21 to +22
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.//! Common MCP (Model Context Protocol)
// infrastructure for Tari applications
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the malformed copyright header.

There's a syntax error in the copyright header where the license text and module documentation are incorrectly merged.

-// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.//! Common MCP (Model Context Protocol)
-// infrastructure for Tari applications
+// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+//! Common MCP (Model Context Protocol) infrastructure for Tari applications
🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/input_sanitizer.rs around lines 21 to
22, the copyright header is malformed because the license text and module
documentation are merged incorrectly. Separate the license comment block from
the module documentation comment by ensuring the license text is properly
commented as a block comment and the module documentation starts with triple
slashes (///) on its own line to fix the syntax error.

Comment on lines +145 to +148
sanitized = sanitized
.chars()
.filter(|&c| c == '\n' || c == '\t' || c == '\r' || (c >= ' ' && c != '\u{007F}') || c >= '\u{0080}')
.collect();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify the character filtering logic for security completeness.

The control character filtering allows newlines, tabs, and carriage returns, but the logic may have edge cases. The condition c >= '\u{0080}' allows all Unicode characters above the ASCII range without validation.


🏁 Script executed:

#!/bin/bash
# Description: Verify if there are known security issues with allowing all high Unicode characters
# Expected: Check if the current filtering approach is sufficient for security

echo "Checking Unicode character ranges and potential security concerns..."
echo "Current filter allows:"
echo "- Newlines (\\n)"  
echo "- Tabs (\\t)"
echo "- Carriage returns (\\r)"
echo "- Printable ASCII (>= space, != DEL)"
echo "- All Unicode above \\u{0080}"
echo ""
echo "Potentially problematic characters that would be allowed:"
python3 -c "
import unicodedata

# Check for potentially dangerous Unicode characters
dangerous_categories = ['Cf', 'Cc']  # Format controls, other controls
sample_chars = [
    '\u200E',  # Left-to-right mark
    '\u200F',  # Right-to-left mark  
    '\u202A',  # Left-to-right embedding
    '\u202B',  # Right-to-left embedding
    '\u202C',  # Pop directional formatting
    '\u202D',  # Left-to-right override
    '\u202E',  # Right-to-left override
    '\uFEFF',  # Byte order mark
]

print('Potentially dangerous Unicode characters that current filter allows:')
for char in sample_chars:
    category = unicodedata.category(char)
    name = unicodedata.name(char, 'UNKNOWN')
    print(f'  \\u{ord(char):04X} ({category}): {name}')
"

Length of output: 1684


🏁 Script executed:

#!/bin/bash
echo "Listing potentially problematic Unicode characters allowed by current filter:"
python3 - << 'EOF'
import unicodedata

codes = [
    0x200E,  # Left-to-right mark
    0x200F,  # Right-to-left mark
    0x202A,  # Left-to-right embedding
    0x202B,  # Right-to-left embedding
    0x202C,  # Pop directional formatting
    0x202D,  # Left-to-right override
    0x202E,  # Right-to-left override
    0xFEFF,  # Byte order mark
]

for code in codes:
    char = chr(code)
    print(f"U+{code:04X}: category={unicodedata.category(char)}, name={unicodedata.name(char, 'UNKNOWN')}")
EOF

Length of output: 569


Disallow Unicode Format Control Characters in Input Sanitizer

The current filter in
applications/minotari_mcp_common/src/input_sanitizer.rs (lines 145–148)
allows every codepoint ≥ U+0080, including “Cf” (format control) characters such as U+200E (LTR mark), U+202E (RTL override), U+FEFF (BOM), etc. These can be abused for visual obfuscation or RLO attacks.

• File: applications/minotari_mcp_common/src/input_sanitizer.rs
Lines: 145–148

Suggested change—explicitly exclude Unicode format controls (category Cf), for example by using a Unicode-categories crate or manual blacklist:

- sanitized = sanitized
-     .chars()
-     .filter(|&c| c == '\n' || c == '\t' || c == '\r'
-         || (c >= ' ' && c != '\u{007F}') || c >= '\u{0080}')
-     .collect();
+ use unicode_categories::UnicodeCategories;  // add to Cargo.toml
+
+ sanitized = sanitized
+     .chars()
+     .filter(|&c| {
+         // allow basic whitespace & printable ASCII
+         matches!(c, '\n' | '\t' | '\r')
+             || (c >= ' ' && c != '\u{007F}')
+         // for non-ASCII, drop format controls
+             && !(c.is_general_category_format())
+     })
+     .collect();

If you prefer not to pull in a crate, at minimum blacklist known ranges:

.filter(|&c| {
    /* …same as above… */
    && !matches!(c,
        '\u{200E}'|'\u{200F}'|
        '\u{202A}'..='\u{202E}'|
        '\u{FEFF}'
    )
})

This ensures directional overrides and zero-width markers are stripped out.

🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/input_sanitizer.rs lines 145 to 148, the
current character filter allows all codepoints >= U+0080, including Unicode
format control characters (category Cf) like U+200E, U+202E, and U+FEFF, which
can be abused for visual obfuscation. To fix this, update the filter to
explicitly exclude these format control characters by adding a condition that
blacklists them, for example using a matches! macro to exclude '\u{200E}',
'\u{200F}', the range '\u{202A}' to '\u{202E}', and '\u{FEFF}', ensuring these
characters are filtered out from the sanitized input.

Comment on lines +179 to +184
if obj.len() > self.max_array_length {
return Err(McpError::invalid_request(format!(
"Object too large ({} properties, max {})",
obj.len(),
self.max_array_length
)));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix inconsistent object size validation.

The object size validation uses max_array_length instead of a dedicated object size limit, which is confusing and may not reflect the intended behavior.

Consider adding a dedicated max_object_size field or clearly document why max_array_length is reused for objects:

         // Check object size limit
-        if obj.len() > self.max_array_length {
+        if obj.len() > self.max_array_length {  // Note: reuses array limit for simplicity
             return Err(McpError::invalid_request(format!(
-                "Object too large ({} properties, max {})",
+                "Object has too many properties ({}, max {})",
                 obj.len(),
                 self.max_array_length
             )));

Alternatively, add a dedicated field:

 pub struct InputSanitizer {
     max_string_length: usize,
     max_array_length: usize,
+    max_object_properties: usize,
     max_object_depth: usize,
     allowed_html_entities: HashMap<String, String>,
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/input_sanitizer.rs around lines 179 to
184, the object size validation incorrectly uses the max_array_length field,
causing confusion and potential misbehavior. To fix this, introduce a dedicated
max_object_size field in the struct to represent the maximum allowed object
size, update the validation logic to use max_object_size instead of
max_array_length, and adjust any related documentation to clarify the
distinction between array length and object size limits.

Comment on lines +221 to +225
if sanitized_path.contains("..") {
return Err(McpError::invalid_request(
"Path cannot contain '..' (directory traversal not allowed)",
));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Strengthen directory traversal protection.

The current check only looks for literal ".." strings, which can be bypassed with URL encoding, Unicode normalization, or other obfuscation techniques.

         // Check for directory traversal attempts
-        if sanitized_path.contains("..") {
+        if sanitized_path.contains("..") || sanitized_path.contains("%2e%2e") || sanitized_path.contains("%2E%2E") {
             return Err(McpError::invalid_request(
-                "Path cannot contain '..' (directory traversal not allowed)",
+                "Path contains directory traversal patterns",
             ));
         }

Additionally, consider using std::path::Path::canonicalize() for more robust path validation:

use std::path::Path;

// After basic checks, validate the canonical path
if let Ok(canonical) = Path::new(&sanitized_path).canonicalize() {
    if !canonical.starts_with(std::env::current_dir()?) && 
       !canonical.starts_with("/tmp/") {
        return Err(McpError::invalid_request("Path resolves outside allowed directories"));
    }
}
🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/input_sanitizer.rs around lines 221 to
225, the directory traversal check only looks for the literal ".." string, which
can be bypassed by encoding or obfuscation. To fix this, enhance the validation
by using std::path::Path::canonicalize() to resolve the absolute path and verify
it stays within allowed directories like the current working directory or /tmp/.
Replace or supplement the existing check with this canonical path validation to
robustly prevent directory traversal attacks.

Comment on lines +227 to +229
if sanitized_path.starts_with('/') && !sanitized_path.starts_with("/tmp/") {
return Err(McpError::invalid_request("Absolute paths not allowed (except /tmp/)"));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Reconsider the blanket exception for /tmp/ paths.

Allowing any path under /tmp/ may be overly permissive and could lead to security issues, especially in shared hosting environments or containers.

Consider a more restrictive approach:

-        if sanitized_path.starts_with('/') && !sanitized_path.starts_with("/tmp/") {
-            return Err(McpError::invalid_request("Absolute paths not allowed (except /tmp/)"));
+        if sanitized_path.starts_with('/') {
+            // Only allow specific whitelisted absolute paths
+            let allowed_prefixes = ["/tmp/tari_mcp/", "/tmp/minotari/"];
+            if !allowed_prefixes.iter().any(|prefix| sanitized_path.starts_with(prefix)) {
+                return Err(McpError::invalid_request("Absolute path not in allowed directories"));
+            }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if sanitized_path.starts_with('/') && !sanitized_path.starts_with("/tmp/") {
return Err(McpError::invalid_request("Absolute paths not allowed (except /tmp/)"));
}
if sanitized_path.starts_with('/') {
// Only allow specific whitelisted absolute paths
let allowed_prefixes = ["/tmp/tari_mcp/", "/tmp/minotari/"];
if !allowed_prefixes.iter().any(|prefix| sanitized_path.starts_with(prefix)) {
return Err(McpError::invalid_request("Absolute path not in allowed directories"));
}
}
🤖 Prompt for AI Agents
In applications/minotari_mcp_common/src/input_sanitizer.rs around lines 227 to
229, the current code allows any absolute path starting with /tmp/, which is too
permissive and may cause security risks. Modify the condition to restrict
allowed /tmp/ paths more narrowly, for example by allowing only specific
subdirectories or validating the path against a whitelist, instead of permitting
all paths under /tmp/.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
applications/minotari_mcp_common/src/process_manager.rs (3)

74-78: Critical: _shutdown_tx is dropped - supervisor stops instantly.

The shutdown sender is discarded, causing shutdown_rx.recv().await to return None immediately and the supervisor to exit prematurely. Return the sender from new() so callers can signal shutdown properly.


237-241: Use async TCP connection to avoid blocking the executor.

The blocking std::net::TcpStream::connect can block the async executor. Use tokio::net::TcpStream::connect().await instead.


314-316: Use async TCP connection to avoid blocking the executor.

The blocking std::net::TcpStream::connect can block the async executor. Use tokio::net::TcpStream::connect().await instead.

🧹 Nitpick comments (2)
applications/minotari_mcp_common/src/process_manager.rs (2)

395-411: Expand test coverage for utility functions.

Consider adding tests for:

  • find_available_port() with known available/unavailable ports
  • is_port_available() functionality
  • build_node_command() and build_wallet_command() argument generation
  • Edge cases like invalid ports or missing executables

88-89: Consider making supervisor parameters configurable.

The max_restart_attempts, restart_delay_secs, and MAX_HEALTH_ATTEMPTS are hardcoded. Consider making these configurable for different deployment scenarios.

Also applies to: 208-208

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 20ce2e4 and 27df1e7.

📒 Files selected for processing (1)
  • applications/minotari_mcp_common/src/process_manager.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: MCozhusheck
PR: tari-project/tari#7262
File: applications/minotari_node/src/grpc/base_node_grpc_server.rs:533-536
Timestamp: 2025-06-30T06:54:18.170Z
Learning: In the Tari codebase, readiness status reporting is split between two gRPC servers: base_node_grpc_server's GetNetworkState method always reports READY when it can respond (indicating network-level readiness), while readiness_grpc_server handles granular readiness states like database initialization and migration progress via ReadinessStatusHandler.
applications/minotari_mcp_common/src/process_manager.rs (8)

Learnt from: hansieodendaal
PR: #7280
File: base_layer/core/src/chain_storage/blockchain_database.rs:361-438
Timestamp: 2025-07-04T10:56:46.079Z
Learning: The combination of tokio::task::spawn(async move { with tokio::task::spawn_blocking().await in the payref rebuild background task works well and shuts down properly with the tokio environment, as confirmed by testing in the Tari codebase.

Learnt from: hansieodendaal
PR: #7294
File: comms/dht/src/network_discovery/seed_strap.rs:352-456
Timestamp: 2025-07-09T08:33:29.320Z
Learning: In comms/dht/src/network_discovery/seed_strap.rs, the context.connectivity.dial_peer method should fail fast and return an error if a peer cannot be dialed, rather than requiring retry logic for general connection failures.

Learnt from: hansieodendaal
PR: #7294
File: comms/dht/src/network_discovery/seed_strap.rs:352-456
Timestamp: 2025-07-09T08:33:29.320Z
Learning: In comms/dht/src/network_discovery/seed_strap.rs, the fetch_peers_from_connection and collect_peer_stream functions rely on RPC streaming, and when the main connection is closed by another process, collect_peer_stream times out after STREAM_ITEM_TIMEOUT because it cannot detect that the peer can no longer respond, returning an empty vector of peers. This is why the connection state check is important for the retry logic.

Learnt from: hansieodendaal
PR: #7307
File: comms/core/src/connection_manager/peer_connection.rs:356-357
Timestamp: 2025-07-12T03:43:22.545Z
Learning: In base_layer/core/src/base_node/tari_pulse_service/mod.rs, the disconnect_if_unused call uses parameters (0, 2) because the tari_pulse_service creates exactly 2 substreams and 0 RPC sessions during health checks.

Learnt from: hansieodendaal
PR: #7307
File: comms/core/src/connection_manager/peer_connection.rs:356-357
Timestamp: 2025-07-12T03:43:22.545Z
Learning: In comms/core/src/connectivity/manager.rs, the disconnect_if_unused call uses parameters (0, 0) intentionally to be conservative and avoid terminating connections that might be in use. This approach is used while troubleshooting inbound connection drop issues to eliminate the disconnect logic as a potential cause.

Learnt from: SWvheerden
PR: #6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.

Learnt from: SWvheerden
PR: #6951
File: base_layer/core/src/base_node/tari_pulse_service/mod.rs:327-352
Timestamp: 2025-04-16T07:06:53.981Z
Learning: The discovery_peer and dial_peer methods in the Tari codebase have built-in timeout mechanisms, so adding explicit timeouts with tokio::time::timeout is unnecessary.

Learnt from: hansieodendaal
PR: #7284
File: applications/minotari_console_wallet/src/automation/commands.rs:0-0
Timestamp: 2025-07-15T12:23:14.650Z
Learning: In applications/minotari_console_wallet/src/automation/commands.rs, the consistent error handling pattern for command execution is to use match statements that: 1) On success: log with debug!, print user feedback, and push tx_id to tx_ids vector for monitoring, 2) On error: print error message with eprintln! using the format "{CommandName} error! {}", rather than using .unwrap() which would panic.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: cargo check with stable
  • GitHub Check: test (nextnet, nextnet)
  • GitHub Check: test (mainnet, stagenet)
  • GitHub Check: test (testnet, esmeralda)
  • GitHub Check: ci
  • GitHub Check: ledger build tests

@SWvheerden SWvheerden merged commit 24a856c into tari-project:development Jul 21, 2025
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants