Skip to content

feat(mcp): add BDD testing framework and modularize HTTP bridge#88

Merged
sergiofilhowz merged 1 commit intomainfrom
fix/mcp-index-skill
May 6, 2026
Merged

feat(mcp): add BDD testing framework and modularize HTTP bridge#88
sergiofilhowz merged 1 commit intomainfrom
fix/mcp-index-skill

Conversation

@sergiofilhowz
Copy link
Copy Markdown
Contributor

@sergiofilhowz sergiofilhowz commented May 6, 2026

  • Introduced a BDD testing framework using Cucumber for behavior-driven development.
  • Added new feature tests for error handling, initialization, notifications, prompts, resources, and tools.
  • Modularized the HTTP bridge by creating a dedicated lib.rs for the MCP library.
  • Updated Cargo.toml to include new dependencies for BDD testing and futures.
  • Removed outdated integration tests and manifest tests to streamline the testing process.
  • Enhanced the skills bridge to better handle nested URIs in markdown.

This commit lays the groundwork for comprehensive testing of the MCP worker's functionality and improves the overall architecture of the codebase.

Summary by CodeRabbit

  • Tests

    • Introduced comprehensive Behavior-Driven Development (BDD) testing framework with feature files covering protocol initialization, error handling, notifications, prompts, resources, and tools functionality.
    • Added extensive test step definitions and test infrastructure for automated scenario validation.
  • Bug Fixes

    • Improved skill description extraction to preserve full first paragraphs instead of truncating them.

- Introduced a BDD testing framework using Cucumber for behavior-driven development.
- Added new feature tests for error handling, initialization, notifications, prompts, resources, and tools.
- Modularized the HTTP bridge by creating a dedicated `lib.rs` for the MCP library.
- Updated `Cargo.toml` to include new dependencies for BDD testing and futures.
- Removed outdated integration tests and manifest tests to streamline the testing process.
- Enhanced the skills bridge to better handle nested URIs in markdown.

This commit lays the groundwork for comprehensive testing of the MCP worker's functionality and improves the overall architecture of the codebase.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

This PR introduces BDD testing infrastructure for the MCP crate via Cucumber, refactors module organization by extracting public modules into a library target, updates the nested-URI regex pattern in skills_bridge for improved depth-awareness, and removes legacy integration and manifest tests.

Changes

BDD Testing Infrastructure & Module Reorganization

Layer / File(s) Summary
Library Setup
mcp/Cargo.toml, mcp/src/lib.rs
Library target iii_mcp is added; public modules (config, functions, handler, jsonrpc, manifest, skills_bridge, tools, transport) are exported from src/lib.rs; dev-dependencies cucumber, futures, and uuid are introduced; BDD test target is registered.
Module Refactoring
mcp/src/main.rs
Imports config, functions, and manifest from the iii_mcp crate instead of declaring them locally as modules.
Skills Bridge Logic
mcp/src/skills_bridge.rs
Nested-URI regex pattern is tightened to iii://[^/\s)]+/[^/\s)]+/[^\s)]+ to match depth-2+ URIs more precisely; test strip_nested_lines_keeps_root_and_first_level replaces prior test and validates that root and first-level URIs are preserved while deeper URIs are removed.
Test Harness & World
mcp/tests/bdd.rs, mcp/tests/common/world.rs, mcp/tests/common/mod.rs
BDD harness initializes a Tokio async runner with a shared engine, enforces single-scenario concurrency, and registers a before-hook; McpWorld struct holds per-scenario state (iii handle, HTTP client, URL, unique ID, stash).
Engine & HTTP Helpers
mcp/tests/common/engine.rs, mcp/tests/common/http.rs
Engine module provides get_or_init() to connect, register the MCP handler and trigger, and cache the III handle; HTTP module offers post_jsonrpc() to POST JSON-RPC requests, parse SSE responses, and store results in the world stash; accessors (last_status, last_body, last_rpc) retrieve stored values.
Feature Specifications
mcp/tests/features/*.feature
Seven feature files (errors, initialize, manifest, notifications, prompts, resources, tools) define Gherkin scenarios for JSON-RPC error handling, protocol handshake, manifest validation, notification dispatch, prompts and resources delegation, and tools listing.
Step Implementations
mcp/tests/steps/*.rs, mcp/tests/steps/mod.rs
Step definitions for each feature: errors (malformed requests), initialize (handshake), manifest (CLI validation), notifications (202 responses), prompts (delegation), resources (delegation), tools (listing and hidden-prefix checks).
Test Cleanup
mcp/tests/integration.rs, mcp/tests/manifest.rs
Legacy integration and manifest tests are removed; BDD infrastructure supersedes them.

Skills Description Extraction Enhancement

Layer / File(s) Summary
Extraction Logic
skills/src/functions/skills.rs
extract_description() now returns the full first paragraph without truncation; updated test extract_description_keeps_long_first_paragraph verifies long first paragraphs are preserved intact.
Feature Test
skills/tests/features/markdown.feature
New scenario added to validate that long first paragraphs are extracted in full without ellipsis.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • iii-hq/workers#81: Modifies skills/src/functions/skills.rs in overlapping areas, including the description extraction function.
  • iii-hq/workers#74: Touches the same mcp crate modules (Cargo.toml, src/lib.rs, config, functions) and BDD test structure.
  • iii-hq/workers#82: Overlaps significantly on mcp files (src/lib.rs, src/main.rs, skills_bridge) and test harness setup.

Suggested reviewers

  • ytallo

🐇 Hops with joy, whiskers twitching

Tests now bloom where features grow,
Gherkin scripts dance row by row,
Descriptions long now stretch and flow,
Descriptions long now stretch and flow,
The bridge runs strong, the world's aglow!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(mcp): add BDD testing framework and modularize HTTP bridge' accurately summarizes the main changes: introducing BDD testing infrastructure (Cucumber) and restructuring the MCP crate into a library module, which are the primary objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mcp-index-skill

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (2)
mcp/tests/common/http.rs (1)

63-66: ⚡ Quick win

parse_sse_opt captures the first data: line — should capture the last

MCP Streamable HTTP allows a server to emit intermediate SSE events (e.g., progress notifications) before the final result event. find_map stops at the first data: line, so if the bridge ever starts sending a leading notification event, all Then assertions that call last_rpc would silently inspect the notification envelope instead of the result, producing misleading failures like "expected result.prompts to be an array".

♻️ Proposed fix — capture the last data line
 pub fn parse_sse_opt(body: &str) -> Option<Value> {
-    let line = body.lines().find_map(|l| l.strip_prefix("data: "))?;
+    let line = body.lines().filter_map(|l| l.strip_prefix("data: ")).last()?;
     serde_json::from_str(line).ok()
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp/tests/common/http.rs` around lines 63 - 66, The current parse_sse_opt
function uses body.lines().find_map to grab the first "data: " line, but it
needs to capture the final SSE data envelope (the last "data: " line) so test
helpers like last_rpc inspect the final result; change the line-selection logic
in parse_sse_opt to search from the end (e.g., iterate lines in reverse or
collect and take the last element) to pick the last line that strips the "data:
" prefix, then pass that to serde_json::from_str(line).ok() as before.
mcp/tests/steps/tools.rs (1)

44-54: ⚡ Quick win

Hardcoded bad prefix list diverges from default_hidden_prefixes() over time.

The list manually encodes the ::__ transformation of every prefix in config::default_hidden_prefixes(). Adding a new hidden prefix to the config requires a matching update here, and there's no compile-time link to enforce it.

Consider deriving the list programmatically from the actual defaults:

♻️ Proposed refactor
+use iii_mcp::config::default_hidden_prefixes;
 
 #[then("no advertised tool name starts with a hidden prefix")]
 fn no_hidden_prefix_in_tools(world: &mut McpWorld) {
     if world.iii.is_none() {
         return;
     }
     let v = last_rpc(world);
     let arr = v["result"]["tools"].as_array().unwrap_or_else(|| {
         panic!("expected result.tools to be an array, got {v:#}");
     });
-    // The MCP tool name encoding maps `::` to `__`, so check the
-    // matching prefixes too.
-    let bad: &[&str] = &[
-        "engine__",
-        "state__",
-        "stream__",
-        "iii.",
-        "iii__",
-        "mcp__",
-        "a2a__",
-        "skills__",
-        "prompts__",
-    ];
+    // Derive encoded prefixes from the canonical config defaults.
+    let encoded: Vec<String> = default_hidden_prefixes()
+        .into_iter()
+        .map(|p| p.replace("::", "__"))
+        .collect();
+    let bad: Vec<&str> = encoded.iter().map(|s| s.as_str()).collect();
     for tool in arr {
         let name = tool["name"].as_str().unwrap_or("");
         for prefix in bad {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp/tests/steps/tools.rs` around lines 44 - 54, The test currently hardcodes
the `bad` prefixes (in `bad: &[&str]`) by manually encoding the `::`→`__`
transformation, which can drift from `config::default_hidden_prefixes()`; change
the test to derive `bad` programmatically by calling
`config::default_hidden_prefixes()` and mapping each prefix with the same `::` →
`__` transformation used by production code (e.g., replace("::", "__")), then
use that generated collection (Vec<String> or Vec<&str>/iter comparison) in
place of the hardcoded slice so the test always reflects the real defaults
(refer to `default_hidden_prefixes()` and the `bad` variable).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mcp/tests/features/notifications.feature`:
- Around line 1-16: Add assertions in both scenarios of notifications.feature to
verify the response body is empty: after the existing "Then the http response
status is 202" step, add a step that calls the test helper to assert body
emptiness (use the existing notifications.rs step that invokes
last_body(world).is_empty()). Update or add the corresponding step
implementation in notifications.rs to call last_body(world).is_empty() and fail
the step if it returns false, ensuring the spec "with no body" is actually
enforced.

---

Nitpick comments:
In `@mcp/tests/common/http.rs`:
- Around line 63-66: The current parse_sse_opt function uses
body.lines().find_map to grab the first "data: " line, but it needs to capture
the final SSE data envelope (the last "data: " line) so test helpers like
last_rpc inspect the final result; change the line-selection logic in
parse_sse_opt to search from the end (e.g., iterate lines in reverse or collect
and take the last element) to pick the last line that strips the "data: "
prefix, then pass that to serde_json::from_str(line).ok() as before.

In `@mcp/tests/steps/tools.rs`:
- Around line 44-54: The test currently hardcodes the `bad` prefixes (in `bad:
&[&str]`) by manually encoding the `::`→`__` transformation, which can drift
from `config::default_hidden_prefixes()`; change the test to derive `bad`
programmatically by calling `config::default_hidden_prefixes()` and mapping each
prefix with the same `::` → `__` transformation used by production code (e.g.,
replace("::", "__")), then use that generated collection (Vec<String> or
Vec<&str>/iter comparison) in place of the hardcoded slice so the test always
reflects the real defaults (refer to `default_hidden_prefixes()` and the `bad`
variable).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21754251-6848-4040-b61e-edd8a4142a0f

📥 Commits

Reviewing files that changed from the base of the PR and between 846f9c2 and b6a0f18.

⛔ Files ignored due to path filters (2)
  • mcp/Cargo.lock is excluded by !**/*.lock
  • skills/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • mcp/Cargo.toml
  • mcp/src/lib.rs
  • mcp/src/main.rs
  • mcp/src/skills_bridge.rs
  • mcp/tests/bdd.rs
  • mcp/tests/common/engine.rs
  • mcp/tests/common/http.rs
  • mcp/tests/common/mod.rs
  • mcp/tests/common/world.rs
  • mcp/tests/features/errors.feature
  • mcp/tests/features/initialize.feature
  • mcp/tests/features/manifest.feature
  • mcp/tests/features/notifications.feature
  • mcp/tests/features/prompts.feature
  • mcp/tests/features/resources.feature
  • mcp/tests/features/tools.feature
  • mcp/tests/integration.rs
  • mcp/tests/manifest.rs
  • mcp/tests/steps/errors.rs
  • mcp/tests/steps/initialize.rs
  • mcp/tests/steps/manifest.rs
  • mcp/tests/steps/mod.rs
  • mcp/tests/steps/notifications.rs
  • mcp/tests/steps/prompts.rs
  • mcp/tests/steps/resources.rs
  • mcp/tests/steps/tools.rs
  • skills/src/functions/skills.rs
  • skills/tests/features/markdown.feature
💤 Files with no reviewable changes (2)
  • mcp/tests/manifest.rs
  • mcp/tests/integration.rs

Comment on lines +1 to +16
@engine @notifications
Feature: notifications return 202 Accepted with no body
MCP Streamable HTTP §"server response": notification-only requests
(no `id` field) MUST return 202. The bridge accepts both the
spec'd `notifications/initialized` and any unknown notification.

Background:
Given the iii engine is reachable

Scenario: notifications/initialized returns 202
When I send a notifications/initialized notification
Then the http response status is 202

Scenario: an unknown notification still returns 202
When I send an unknown notification with method "notifications/something-novel"
Then the http response status is 202
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

"With no body" is stated but not asserted

Both the Feature description and module doc promise 202 with no body, but neither scenario includes a Then step verifying the empty response body. The 202 status is tested, but the body-emptiness half of the contract is unverified, leaving room for a regression where the bridge starts returning a body without breaking these tests.

💡 Suggested addition to both scenarios
  Scenario: notifications/initialized returns 202
    When I send a notifications/initialized notification
    Then the http response status is 202
+   And  the response body is empty

  Scenario: an unknown notification still returns 202
    When I send an unknown notification with method "notifications/something-novel"
    Then the http response status is 202
+   And  the response body is empty

You would add a matching step in notifications.rs using last_body(world).is_empty().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcp/tests/features/notifications.feature` around lines 1 - 16, Add assertions
in both scenarios of notifications.feature to verify the response body is empty:
after the existing "Then the http response status is 202" step, add a step that
calls the test helper to assert body emptiness (use the existing
notifications.rs step that invokes last_body(world).is_empty()). Update or add
the corresponding step implementation in notifications.rs to call
last_body(world).is_empty() and fail the step if it returns false, ensuring the
spec "with no body" is actually enforced.

@sergiofilhowz sergiofilhowz merged commit 16e680c into main May 6, 2026
9 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.

1 participant