Skip to content

feat(mcp,a2a): opt-in tool exposure with hard floor + tier filter + no-builtins#37

Merged
rohitg00 merged 4 commits intomainfrom
feat/mcp-a2a-exposure-hardening
Apr 23, 2026
Merged

feat(mcp,a2a): opt-in tool exposure with hard floor + tier filter + no-builtins#37
rohitg00 merged 4 commits intomainfrom
feat/mcp-a2a-exposure-hardening

Conversation

@rohitg00
Copy link
Copy Markdown
Contributor

@rohitg00 rohitg00 commented Apr 23, 2026

Problem

Running iii-mcp over Streamable HTTP works cleanly — inspector hits `POST /mcp`, pulls the 6 builtins + every registered worker function as tools. But at scale the tool list turns into noise:

  • 3-4 workers × 5-6 functions each → 25-30 tools before the agent even reads the user's message.
  • Engine internals like `state::set`, `stream::`, `engine::` can leak when anyone flips `--expose-all` for one legitimate worker. No hard floor.
  • Binary expose/hide — no way to say "Claude Desktop gets the 4 user-facing tools, the agent orchestrator gets those plus 10 more, ops gets all of them."

Same problem applies to the A2A agent card.

Fix

Three layers of gate, both workers:

  1. Hard floor — `ALWAYS_HIDDEN_PREFIXES` (`engine::`, `state::`, `stream::`, `iii.`, plus the worker's own namespace). Never surfaced, even under `--expose-all`. Mirrors the `agent` worker's existing `DEFAULT_EXCLUDED_PREFIXES`.
  2. Opt-in metadata — `mcp.expose: true` / `a2a.expose: true` stays as the explicit gate. No change to the flag name or semantics.
  3. Tier filter — new optional string `mcp.tier` / `a2a.tier` on function metadata. Server `--tier ` CLI flag filters further. One worker → multiple audiences.

MCP also gets `--no-builtins`

The 6 management tools (`iii_worker_`, `iii_trigger_`) are useful over stdio (Claude Desktop can spawn workers) but pure noise over HTTP (they reject with "requires stdio"). Default: on for stdio, off for HTTP.

Metadata schema (worker authors)

```rust
metadata: Some(json!({
"mcp.expose": true, // opt-in gate
"mcp.tier": "agent", // optional — free-form string
"a2a.expose": true, // same pattern for A2A
"a2a.tier": "partner",
}))
```

CLI

```
iii-mcp [--no-stdio] [--expose-all] [--no-builtins] [--tier ]
iii-a2a [--expose-all] [--tier ] [--base-url ]
```

Multi-audience example

```
Claude Desktop user config → iii-mcp --tier user --no-builtins
Agent orchestrator client → iii-mcp --no-stdio --tier agent
Ops dashboard → iii-mcp --no-stdio --tier ops
```

Same engine, three tool lists.

Test plan

  • `cargo fmt --all -- --check` ok on both workers
  • `cargo clippy --all-targets --all-features -- -D warnings` ok on both
  • `cargo test --all-features` passes (0/0 + 0/0 — integration tests not in this PR)
  • Live smoke: `curl -s localhost:3111/mcp` with one exposed worker, expect hidden infra prefixes stay out
  • Live smoke: `curl -s localhost:3111/.well-known/agent-card.json` returns only tagged skills

Summary by CodeRabbit

  • New Features

    • Tier-based function filtering for both Agent‑to‑Agent and MCP via a new --tier option.
    • New --no-builtins flag to hide built-in management tools in MCP.
    • Unified exposure controls that always hide infra namespaces even when exposing all.
  • Documentation

    • New guides covering A2A and MCP registration, exposure/tier rules, CLI options, task persistence, cancellation semantics, and error codes.
  • Bug Fixes

    • Clearer client-facing errors when invoking functions blocked by exposure or tier rules.

…o-builtins

Both workers previously gated on a single boolean metadata flag
(mcp.expose / a2a.expose) with --expose-all as the only escape hatch.
Past ~20 functions the resulting tool list becomes noise: engine
internals sit next to agent-facing functions as if equivalent, and
Claude Desktop / MCP Inspector burn context just reading it. This PR
tightens the surface:

Hard floor — ALWAYS_HIDDEN_PREFIXES (engine::, state::, stream::,
iii., mcp:: or a2a::). These NEVER appear in tools/list or agent-card
skills, even under --expose-all. Matches the agent worker's existing
DEFAULT_EXCLUDED_PREFIXES carve-out.

Tier filter — optional mcp.tier / a2a.tier string on the function
metadata plus a --tier <name> CLI flag. One worker can tag functions
as 'user' / 'agent' / 'ops' and three server instances behind three
different auth proxies show three different tool lists.

no-builtins — new --no-builtins flag on iii-mcp hides the 6 built-in
worker-management tools (iii_worker_register, iii_worker_stop,
iii_trigger_register, iii_trigger_unregister, iii_trigger_void,
iii_trigger_enqueue). Defaults: off for stdio (common Claude Desktop
path needs them), on for --no-stdio HTTP (over HTTP worker
management rejects itself as 'requires stdio' anyway — pure noise).

Internals:

- mcp/handler.rs: new ExposureConfig carrying { expose_all, no_builtins,
  tier } threaded through McpHandler and dispatch_http. is_function_exposed
  centralizes the three gates (hard floor, metadata flag, tier match) and
  is reused in tools/list + tools/call + HTTP tools/call.
- a2a/handler.rs: same ExposureConfig { expose_all, tier } threaded
  through register/build_agent_card/handle_a2a_request/handle_send.
  is_exposed helper for in-hand FunctionInfo, is_function_exposed async
  helper for looking up a function_id before dispatch.
- mcp/main.rs + a2a/main.rs: add --tier CLI, mcp adds --no-builtins.
- docs: add mcp/README.md and a2a/README.md documenting the opt-in
  pattern with Rust/Node/Python registration snippets and a worked
  example of the tier flag across three audiences.
- initialize_result() instructions string updated to describe the new
  gate.

Verification:
- cargo fmt --all -- --check  ok on both
- cargo clippy --all-targets --all-features -- -D warnings  ok on both
- cargo test --all-features  0/0 + 0/0 (integration tests live elsewhere)
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@rohitg00 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 28 minutes and 39 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 28 minutes and 39 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0dcb74ab-5c35-456a-899e-ec87a24c085a

📥 Commits

Reviewing files that changed from the base of the PR and between ad9aa10 and e359bb3.

📒 Files selected for processing (5)
  • a2a/README.md
  • a2a/src/handler.rs
  • mcp/README.md
  • mcp/src/handler.rs
  • mcp/src/main.rs
📝 Walkthrough

Walkthrough

Adds a centralized ExposureConfig (expose_all, optional tier, plus always-hidden prefixes) and applies tier-aware, hard-floor exposure filtering across A2A and MCP; updates CLI flags, handler registration signatures, and documentation; preserves fail-closed listing and refines error messages.

Changes

Cohort / File(s) Summary
A2A Docs
a2a/README.md
New documentation describing A2A JSON-RPC surface, discovery (/.well-known/agent-card.json), /a2a endpoints, task semantics, permission model, CLI flags, and error codes.
A2A Handler & CLI
a2a/src/handler.rs, a2a/src/main.rs
Introduce ExposureConfig and ALWAYS_HIDDEN_PREFIXES; replace expose_all bool with ExposureConfig in register(); add tier-aware exposure checks and improved not-exposed diagnostics; add --tier CLI option.
MCP Docs
mcp/README.md
New README describing MCP exposure model, hard-floor namespaces, built-in tools, transport differences, invocation flow, and examples.
MCP Handler & CLI
mcp/src/handler.rs, mcp/src/main.rs
Introduce ExposureConfig (adds no_builtins), ALWAYS_HIDDEN_PREFIXES; refactor registration and handler construction to accept ExposureConfig; apply unified infra/expose/tier filtering for tools/list and tools/call; add --tier and --no-builtins CLI options.
MCP Minor Edits
mcp/src/transport.rs, mcp/src/worker_manager.rs
Cosmetic formatting changes to function signature and error message formatting; no behavioral change.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AgentCard as "Agent Card /.well-known/agent-card.json"
  participant A2A as "A2A /a2a (Handler)"
  participant Registry
  participant TaskStore
  Client->>AgentCard: GET discover base_url / capabilities
  Client->>A2A: POST message/send (function_id in structured data or text)
  A2A->>Registry: resolve function_id
  Registry-->>A2A: function metadata (including a2a.expose, a2a.tier, namespace)
  A2A->>A2A: apply ExposureConfig (expose_all, tier) + ALWAYS_HIDDEN_PREFIXES
  alt exposed
    A2A->>TaskStore: persist task (a2a:tasks) / check idempotent terminal result
    A2A->>Registry: invoke function / stream result
    TaskStore-->>A2A: store result
    A2A-->>Client: return result
  else not exposed
    A2A-->>Client: JSON-RPC error (not-exposed / denied)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 In burrows of code the tiers align,

gates hum softly, namespaces shy,
hidden floors keep secrets fine,
functions bloom where flags comply,
a rabbit cheers: "Expose with pride!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: opt-in exposure model with hard-floor blocks, tier filtering, and no-builtins flag across mcp and a2a.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-a2a-exposure-hardening

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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
a2a/src/handler.rs (1)

434-452: ⚠️ Potential issue | 🟡 Minor

Misleading rejection message: doesn't reflect hard-floor or tier gates.

The error text blames only a2a.expose metadata, but is_function_exposed rejects for three distinct reasons: hard-floor namespace, missing a2a.expose (when --expose-all is off), and a2a.tier mismatch (when --tier is set). An operator invoking engine::foo or a function with the wrong tier will be told to check a2a.expose, which is the wrong fix.

Consider a message that doesn't pin a specific reason, or have is_function_exposed return a discriminated reason so the surface matches reality:

🛠️ Proposed fix
                 parts: vec![text_part(format!(
-                    "Function '{}' is not exposed via a2a.expose metadata",
-                    function_id
+                    "Function '{}' is not exposed (check hard-floor namespaces, a2a.expose, and --tier)",
+                    function_id
                 ))],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@a2a/src/handler.rs` around lines 434 - 452, The rejection text incorrectly
blames only a2a.expose even though is_function_exposed can fail for hard-floor
namespace, missing expose when --expose-all is off, or a2a.tier mismatch; update
the code so the rejection message accurately reflects the real cause by either
(A) changing is_function_exposed to return a discriminated enum (e.g.
ExposeRejection::HardFloor | ::NotExposed | ::TierMismatch) and use that enum in
the handler to set TaskStatus/Message with a specific, correct message, or (B)
if you prefer a quicker change, replace the current hardcoded error text around
function_id with a neutral message such as "Function is not allowed to be
invoked by current namespace/exposure/tier settings" so the user isn't misled;
refer to is_function_exposed, TaskStatus, TaskState, Message and function_id
when implementing the fix.
mcp/src/handler.rs (1)

352-378: ⚠️ Potential issue | 🟠 Major

Filter the functions resource with the same exposure rules.

iii://functions still returns raw list_functions() output, so MCP clients can enumerate hard-hidden or tier-filtered functions even when tools/list hides them. Apply is_function_exposed here too, or remove this resource from unprivileged MCP surfaces.

Suggested fix
             "iii://functions" => {
-                let v = self
+                let functions = self
                     .iii
                     .list_functions()
                     .await
                     .map_err(|e| format!("{}", e))?;
+                let v: Vec<_> = functions
+                    .into_iter()
+                    .filter(|f| is_function_exposed(f, &self.exposure))
+                    .collect();
                 (
                     serde_json::to_string_pretty(&v).unwrap_or_else(|_| "[]".into()),
                     "application/json",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mcp/src/handler.rs` around lines 352 - 378, resources_read currently returns
the raw output of iii.list_functions() for "iii://functions", exposing
hidden/tier-filtered entries; update the "iii://functions" branch in
resources_read to filter the listed functions using the same is_function_exposed
predicate used by tools/list (call is_function_exposed for each function and
only include exposed ones) before serializing, or alternatively remove
"iii://functions" from resources_list to avoid exposing the resource to
unprivileged MCP surfaces; reference resources_read, resources_list,
is_function_exposed, and iii.list_functions() when applying the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@a2a/README.md`:
- Around line 65-73: The fenced CLI block in README.md is missing a language tag
(MD040); update the opening fence for the CLI snippet that begins with ``` to
use a language identifier (e.g., change the opening fence to ```text or
```console) so the block showing the flags (--engine-url, --expose-all, --tier,
--base-url, --debug) validates and renders predictably.

In `@a2a/src/handler.rs`:
- Line 28: ALWAYS_HIDDEN_PREFIXES currently includes "iii." but not "iii::", so
functions using the SDK double-colon notation (e.g., iii::durable::publish,
iii::config) will slip past the hidden-prefix check; update the constant
ALWAYS_HIDDEN_PREFIXES in handler.rs to include "iii::" (add the string "iii::"
to the slice) so both notations are covered by the prefix-matching logic.

In `@mcp/README.md`:
- Around line 87-90: The README incorrectly documents an unsupported override
(--no-builtins=false); update the text in the README section that mentions "Flip
with `--no-builtins=false`" to remove that override and instead note that the
CLI only provides the negative flag `--no-builtins` and that mcp/src/main.rs
forces HTTP-only mode to hide built-ins, so there is no runtime override; ensure
the sentence clarifies that built-ins remain hidden over HTTP and delete or
replace the false-override example.

In `@mcp/src/handler.rs`:
- Around line 322-333: The current code skips the exposure check when
self.iii.list_functions().await returns Err, allowing calls to proceed to
self.iii.trigger(...) even if exposure cannot be verified; change the logic to
fail closed: if list_functions() returns Err, treat the function as not exposed
and return the same tool_error response instead of continuing. Specifically,
adjust the block that calls self.iii.list_functions().await and the identical
block used around lines 603-617 so that failures from list_functions() set
exposed = false (or immediately return the tool_error), using the existing
helpers is_function_exposed, self.exposure, and function_id to decide and bail
out rather than falling through to iii.trigger.
- Around line 225-229: The current code only hides built-ins from discovery by
using self.exposure.no_builtins when building the tools list (via
builtin_tools()), but it does not block direct execution of built-in tools;
update the tool invocation paths (the HTTP/tools/list and tools/call handlers
and any call-resolution logic in the functions covering the ranges around
241-311, 520-524, and 544-591) to consult self.exposure.no_builtins and refuse
or filter calls to built-in tool names (e.g., iii_trigger_void,
iii_trigger_enqueue) when that flag is set; specifically, add a guard in the
function that resolves and dispatches a tool call (the handler that currently
executes tools by name) to check self.exposure.no_builtins and either return an
error/403 or skip resolving builtin_tools() entries before executing.

In `@mcp/src/main.rs`:
- Around line 70-75: The HTTP handler is currently registered with builtins
visible by default because http_no_builtins is derived from args.no_stdio; add
an explicit opt-in flag (e.g. args.http_builtins) and change the logic so
builtins are hidden by default for HTTP: declare the new boolean flag in your
args parsing (name it http_builtins), compute http_no_builtins as
!args.http_builtins || args.no_builtins || args.no_builtins, or more clearly
http_no_builtins = (!args.http_builtins) || args.no_builtins || args.no_builtins
(ensure you only include the real relevant existing flags once), then pass that
into ExposureConfig::new(...) and call handler::register_http(&iii,
http_exposure) as before; this makes HTTP builtins opt-in via --http-builtins
while preserving existing --no-builtin/--no-stdio behavior.

---

Outside diff comments:
In `@a2a/src/handler.rs`:
- Around line 434-452: The rejection text incorrectly blames only a2a.expose
even though is_function_exposed can fail for hard-floor namespace, missing
expose when --expose-all is off, or a2a.tier mismatch; update the code so the
rejection message accurately reflects the real cause by either (A) changing
is_function_exposed to return a discriminated enum (e.g.
ExposeRejection::HardFloor | ::NotExposed | ::TierMismatch) and use that enum in
the handler to set TaskStatus/Message with a specific, correct message, or (B)
if you prefer a quicker change, replace the current hardcoded error text around
function_id with a neutral message such as "Function is not allowed to be
invoked by current namespace/exposure/tier settings" so the user isn't misled;
refer to is_function_exposed, TaskStatus, TaskState, Message and function_id
when implementing the fix.

In `@mcp/src/handler.rs`:
- Around line 352-378: resources_read currently returns the raw output of
iii.list_functions() for "iii://functions", exposing hidden/tier-filtered
entries; update the "iii://functions" branch in resources_read to filter the
listed functions using the same is_function_exposed predicate used by tools/list
(call is_function_exposed for each function and only include exposed ones)
before serializing, or alternatively remove "iii://functions" from
resources_list to avoid exposing the resource to unprivileged MCP surfaces;
reference resources_read, resources_list, is_function_exposed, and
iii.list_functions() when applying the change.
🪄 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: 8ea5179c-f380-41c1-b065-f641f50faa9a

📥 Commits

Reviewing files that changed from the base of the PR and between 8d02fa4 and de305ed.

📒 Files selected for processing (8)
  • a2a/README.md
  • a2a/src/handler.rs
  • a2a/src/main.rs
  • mcp/README.md
  • mcp/src/handler.rs
  • mcp/src/main.rs
  • mcp/src/transport.rs
  • mcp/src/worker_manager.rs

Comment thread a2a/README.md Outdated
Comment thread a2a/src/handler.rs Outdated
Comment thread mcp/README.md Outdated
Comment thread mcp/src/handler.rs
Comment thread mcp/src/handler.rs Outdated
Comment thread mcp/src/main.rs Outdated
…r message

Local integration test caught two issues:

F1 (major): Under --expose-all, each protocol worker's ALWAYS_HIDDEN_PREFIXES
only blocked its own namespace. mcp --expose-all surfaced a2a::agent_card +
a2a::jsonrpc as MCP tools; a2a --expose-all surfaced mcp::handler as an A2A
skill. Both are stateless RPC dispatchers — routing an MCP call through
mcp::handler recurses into ourselves, and double-enveloping an A2A request
inside MCP (or vice versa) is not a meaningful tool surface. Fix: expand
both hard-floor lists to include the sibling protocol namespace.

F2 (minor): A2A hard-floor rejections reused the metadata-miss message, so
'function is in engine namespace' and 'function forgot the a2a.expose flag'
looked identical to callers. MCP already distinguishes them. Mirror the
distinct phrasing on A2A so operators can tell a policy miss from a
protocol-level block.

Verification (live engine + workers, local run):
- default MCP tools/list: 3 opt-in demo tools, nothing else
- default A2A agent card: 3 opt-in demo skills, nothing else
- --expose-all MCP: no a2a::*, no mcp::handler
- --expose-all A2A: no mcp::handler, no a2a::*
- --tier user: only the user-tier demo tool
- tools/call on hidden function: rejected, metadata-miss message
- tools/call on state:: prefix: rejected, hard-floor message
- message/send on state:: prefix: rejected, distinct hard-floor message

Full test matrix in the PR description.
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.

♻️ Duplicate comments (1)
a2a/src/handler.rs (1)

28-38: ⚠️ Potential issue | 🟠 Major

Hard floor still misses the iii:: SDK notation.

ALWAYS_HIDDEN_PREFIXES covers iii. (dot form) but not iii:: (double-colon form). The engine registers SDK-internal callbacks using the iii:: notation (e.g. iii::durable::publish, iii::config — see introspect/src/functions/diagram.rs), so under --expose-all these would bypass the hard floor and become skills on the agent card. The iii. entry alone doesn’t catch them because starts_with("iii.") doesn't match iii::….

🛡️ Proposed fix
 pub const ALWAYS_HIDDEN_PREFIXES: &[&str] = &[
     "engine::",
     "state::",
     "stream::",
     "iii.",
+    "iii::",
     // Sibling protocol worker entry point. Calling `mcp::handler` via A2A
     // message/send double-envelopes an MCP request inside A2A — not a
     // meaningful skill. Hide symmetrically with mcp's a2a:: carve-out.
     "mcp::",
     "a2a::",
 ];
#!/bin/bash
# Confirm whether any registered functions use the `iii::` notation that would
# slip past the current `iii.` prefix.
rg -nP --type=rust '"iii::[A-Za-z0-9_:]+"' -C1
rg -nP --type=rust '\biii::[a-z_]+::[a-z_]+\b' -C1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@a2a/src/handler.rs` around lines 28 - 38, ALWAYS_HIDDEN_PREFIXES currently
contains "iii." but misses the SDK double-colon notation "iii::", so
SDK-internal callbacks like iii::durable::publish and iii::config can slip past
the hard floor; update the ALWAYS_HIDDEN_PREFIXES array in handler.rs to include
the "iii::" entry (in addition to "iii.") so starts_with checks will hide both
dot and double-colon forms and preserve the hard floor behavior for functions
registered with iii:: prefixes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@a2a/src/handler.rs`:
- Around line 28-38: ALWAYS_HIDDEN_PREFIXES currently contains "iii." but misses
the SDK double-colon notation "iii::", so SDK-internal callbacks like
iii::durable::publish and iii::config can slip past the hard floor; update the
ALWAYS_HIDDEN_PREFIXES array in handler.rs to include the "iii::" entry (in
addition to "iii.") so starts_with checks will hide both dot and double-colon
forms and preserve the hard floor behavior for functions registered with iii::
prefixes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 821df327-b3a3-40b4-8720-8d12715e1fdc

📥 Commits

Reviewing files that changed from the base of the PR and between de305ed and ad9aa10.

📒 Files selected for processing (2)
  • a2a/src/handler.rs
  • mcp/src/handler.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • mcp/src/handler.rs

Handler hardcoded protocolVersion: '2025-11-25' — a future date that no
real MCP client recognizes. MCP Inspector rejected it with:

    Failed to connect to MCP server: Server's protocol version is not
    supported: 2025-11-25

Real spec revisions are stamped dates from spec.modelcontextprotocol.io.
Move to '2025-06-18' (current published spec at the time of this commit).
Bump when moving to a newer one.

Caught by live Inspector smoke test over Streamable HTTP. Post-fix:
Inspector lists tools, calls exposed tools, and gets the expected
rejection on hidden tools on both stdio and HTTP transports.
…licy, builtin invocation gate, resource filter

F1 (hard floor gap). Both workers' ALWAYS_HIDDEN_PREFIXES listed 'iii.' but
missed 'iii::'. The SDK uses both notations — callback-style
'iii.on_functions_available.<uuid>' AND namespace-style 'iii::durable::publish',
'iii::config'. Only the dot form was blocked; double-colon SDK internals
slipped past the hard floor under --expose-all. Add both to the slice on
both workers.

F2 (builtin invocation gate). --no-builtins hid the 6 management tools
from tools/list but tools_call still dispatched them by name. A client that
knew the name could call iii_worker_register on a server that claimed it
had no such tool, bypassing policy. Add BUILTIN_TOOL_NAMES + is_builtin_tool
helper; gate tools/call (stdio AND HTTP dispatch) on
cfg.no_builtins && is_builtin_tool(name).

F3 (fail-open on list_functions error). Both stdio tools_call and HTTP
dispatch_http used 'if let Ok(fns)' — when list_functions errored, the
block was skipped and the call fell through to iii.trigger. An engine
outage hid the policy gate. Flip to fail-closed: list_functions error
returns an explicit tool_error 'exposure could not be verified; denying
call'.

F4 (HTTP builtins default). Previous main.rs computed http_no_builtins =
args.no_builtins || args.no_stdio, which meant stdio-default mode (the
common case) registered HTTP WITH builtins. Flip to hide by default:
http_no_builtins = args.no_builtins || !args.http_builtins. Add
--http-builtins opt-in flag for deploys that want builtins over HTTP.
--no-builtins always wins.

F5 (resource surface bypass). resources_read 'iii://functions' handed out
the raw list_functions() output. A client could skip tools/list and read
the resource to enumerate hidden functions — bypassing mcp.expose, tier,
and hard floor. Apply is_function_exposed filter inside resources_read so
the resource surface matches the tool surface.

Doc fixes:
- mcp/README: correct the bogus --no-builtins=false example; document
  --http-builtins; expand hard-floor table to show iii.* AND iii::* plus
  a2a::*.
- a2a/README: add 'text' language tag to the CLI fenced block (MD040);
  expand hard-floor table to show iii.* AND iii::* plus mcp::*.

Verification (live engine + workers):
- HTTP default: 3 opt-in demo tools, no builtins
- HTTP --http-builtins: 3 demo + 6 builtins
- --no-builtins + tools/call iii_trigger_void: rejected with
  'disabled on this server' message
- stdio resources/read iii://functions: demo::exposed present,
  demo::hidden + state::bogus_demo + engine::* + iii.* + iii::* all absent
- cargo fmt + clippy -D warnings clean on both workers
@rohitg00 rohitg00 merged commit 16cf534 into main Apr 23, 2026
@rohitg00 rohitg00 deleted the feat/mcp-a2a-exposure-hardening branch April 23, 2026 14:29
rohitg00 added a commit that referenced this pull request Apr 23, 2026
Conflict resolved by taking main's richer mcp/src/handler.rs, which
carries the opt-in exposure work merged via #37. Clippy + cargo test
pass across mcp, a2a, agent, shell, coding.

CR findings addressed on chore/fix-merged-workers:

- agent/src/main.rs: replace `let _ = state_set(...)` with match/log.
  The discard swallowed engine errors, so a failed state write showed
  up in logs as "tool cache refreshed" (identical to success).

- shell/src/functions/exec.rs + exec_bg.rs: strict args validation.
  The previous filter_map silently dropped non-string entries, so a
  caller sending `{"args": ["--count", 5]}` would have 5 removed and
  the shell would run with partial arguments. Now returns
  `IIIError::Handler("'args[i]' must be a string (got ...)")`.

- coding/src/functions/execute.rs: `.kill_on_drop(true)` on all four
  tokio Commands (rustc compile, rust bin run, ts runtime, python3).
  Without it, timed-out children kept running in the background and
  accumulated as zombies over time.

- coding/src/templates.rs: `normalize_trigger_config` helper strips a
  leading slash from `api_path` before emitting trigger config. The
  engine prepends `/`, so a user-supplied `"/myworker/greet"` became
  `//myworker/greet` on the wire and returned 404 at invoke time.
  Applied at all six trigger-serialization sites (rust/ts/python
  worker templates + the three single-trigger generators).

- coding/src/templates.rs: migrate the TypeScript template's
  `iii.registerFunction({id, ...}, handler)` object-first form to the
  iii-sdk 0.11 positional signature `(id, handler, {description,
  request_format, response_format})`.
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