Skip to content

feat(acp): support remote HTTP MCP servers - #4271

Draft
custard-pirate wants to merge 6 commits into
block:mainfrom
custard-pirate:agent/acp-http-mcp-servers
Draft

feat(acp): support remote HTTP MCP servers#4271
custard-pirate wants to merge 6 commits into
block:mainfrom
custard-pirate:agent/acp-http-mcp-servers

Conversation

@custard-pirate

@custard-pirate custard-pirate commented Aug 2, 2026

Copy link
Copy Markdown

Summary

  • extend the versioned typed MCP document from buzz-acp: add structured MCP server configuration #4164 with a resolved transport: "http" variant
  • pass ACP Streamable HTTP MCP servers alongside stdio servers without adding a second configuration surface
  • validate adapter HTTP capability on initial startup, lazy wake-up, and every respawn before the pool becomes ready
  • redact stdio environment values, HTTP URLs, and HTTP header values from ACP wire logs and observer events

Dependency and scope

This PR is stacked directly on #4164. That PR owns the versioned document, stdio transport, ordering, validation, redaction foundation, and BUZZ_ACP_MCP_COMMAND compatibility input. The HTTP commits should be folded into #4164 before merge so schema version 1 ships with one stable meaning.

Concrete endpoints and credentials remain project-owned configuration. The shared launch document accepts only final resolved URLs and header values; secret-file and environment selectors are intentionally outside the shared schema.

Security and behavior

  • remote endpoints require HTTPS
  • plain HTTP is accepted only for credential-free literal IPv4 or IPv6 loopback endpoints
  • header values must be non-empty and free of control characters
  • credential-bearing values use redacted Debug implementations and are removed from wire logging and observer frames
  • fallback substring redaction ignores short and common public values to avoid corrupting unrelated diagnostics
  • runtimes without advertised HTTP MCP support fail closed before the pool is marked ready
  • legacy name collisions are checked across both stdio and HTTP servers
  • transport support does not bypass adapter-local tool approval; deployments must pre-authorize only the required per-agent tools and fail closed

Validation

  • cargo test -p buzz-core --lib — 257 passed
  • cargo test -p buzz-acp --lib — 737 passed
  • cargo test -p buzz-acp --test config_env — passed
  • cargo clippy -p buzz-core -p buzz-acp --all-targets -- -D warnings
  • cargo fmt --all --check
  • git diff --check

The fake-adapter coverage includes the exact mixed legacy-stdio plus HTTP session/new payload. Release acceptance additionally requires exercising a harmless tool through each transport with a real adapter and publishing through constrained send.

Copy link
Copy Markdown

@custard-pirate, I had already built #4164 before I came across this PR. It covers named stdio MCP servers, isolated per-server environments, compatibility with the existing command setting and secret-safe launch configuration. This PR covers remote HTTP transport, so the work is complementary, but both currently define the configuration shape that reaches ACP.

The practical win is one reviewable configuration for both local and hosted tools, with credentials scoped to the individual server. I think the clean merge is a single versioned typed document with stdio and http transports, while BUZZ_ACP_MCP_COMMAND remains a compatibility input rather than a second permanent schema. Projects should own concrete endpoints and credentials; portable agent templates should declare logical tool requirements.

I opened #4301 with the broader product shape and proposed PR split. I would like to align these schemas rather than leave Buzz with two permanent configuration surfaces.

@custard-pirate

Copy link
Copy Markdown
Author

Thanks — agreed. A single versioned, typed MCP configuration with stdio and http transports is a cleaner long-term shape than introducing parallel configuration surfaces. Keeping BUZZ_ACP_MCP_COMMAND as a compatibility input also makes sense.

I’m happy to align #4271 with #4164. I’ll review its configuration model and #4301 in more detail, then we can coordinate the shared types and PR boundary so the two changes remain complementary. In particular, I’d like to preserve per-server credential isolation for HTTP servers without putting concrete endpoints or secrets into portable agent templates.

@wolfyy970

Copy link
Copy Markdown

Thanks. I have rebased #4164 and tagged every config entry with its transport. Version 1 remains stdio-only, so #4271 can add HTTP to the same document. Concrete endpoints and credentials should stay with Project connections, outside portable agent templates.

@custard-pirate

Copy link
Copy Markdown
Author

Thanks — I’ve reviewed the rebased #4164 and I’m aligned with this boundary. The tagged ConfiguredMcpServer enum provides the extension point #4271 needs.

Assuming the maintainers are happy with that division of work, I’ll adapt #4271 to add HTTP as another transport in the same versioned document, reusing the existing config path, ordering, validation, and redaction behavior rather than retaining a separate HTTP configuration surface.

Concrete URLs, headers, and credentials will remain Project-owned and outside portable agent templates. I’ll also ensure HTTP credential values receive the same no-log/no-diagnostics treatment as stdio environment values.

@custard-pirate

Copy link
Copy Markdown
Author

@wolfyy970 I have updated #4271 on top of the current #4164 head and removed the separate HTTP configuration surface.

The HTTP transport now extends the same versioned ConfiguredMcpServer document and reuses its ordering, validation, compatibility, and redaction behavior. The HTTP-specific commit is 981d39a; the PR remains draft while the stack evolves.

The local validation is green: 703 buzz-acp library tests, Clippy with warnings denied, formatting, and diff checks. Please flag any place where this has drifted from the schema or boundary you intend for #4164.

@RowanAldean

Copy link
Copy Markdown

@wpfleger96 @tlongwell-block an open question here is if
buzz-agent is intended to gain HTTP MCP, or is remote MCP deliberately
harness-only? That answer decides whether the client-side work is worth
anyone building (have this done btw, ~150 LoC)

I think it'd be great to configure tools once in Buzz and bind them to a team
instead of hand-wiring each agent's harness config - which I think is the same
direction #4735 is heading 🚀

Copy link
Copy Markdown

Not harness-only. buzz-acp should resolve the server list and pass it in session/new; the ACP agent owns the MCP session. buzz-agent already owns the stdio lifecycle, so HTTP belongs beside it there.

I’m retiring #4735 as the implementation branch and restacking the binding work around the relay-canonical agent path in #4940. The useful seam remains: Project setup resolves into the same MCP document for any runtime. If your client work is ready, please put it up against #4271’s HTTP transport.

Copy link
Copy Markdown

Thanks for aligning this. I have replaced #4164 with the reviewed four-commit stack on current main. The ConfiguredMcpServer shape and stdio boundary are unchanged; only the commit IDs changed because I removed the repair history.

The stack now includes a real two-server, two-session agent proof. Please rebase your HTTP commit onto the new #4164 head and I will review it against the shared validation and redaction cases.

@custard-pirate

Copy link
Copy Markdown
Author

Rebased onto the replacement #4164 four-commit stack at 780561c. HTTP is now an additional variant of the shared buzz-core MCP launch document; harness-only URL/header credential resolution, ACP capability gating, and wire/observer redaction remain in place. Local validation passed: 713 buzz-acp unit tests, 256 buzz-core unit tests, strict Clippy for both crates with warnings denied, formatting, and diff checks. The HTTP commit is now ddce720 and is ready for the requested review against the shared validation and redaction cases.

@custard-pirate

Copy link
Copy Markdown
Author

@wolfyy970#4271 has now been updated and rebased onto the replacement #4164 head at 780561c. The HTTP change is the single child commit ddce720 and extends the shared buzz-core ConfiguredMcpServer document rather than defining a parallel schema. The affected suites are green: 713 buzz-acp tests, 256 buzz-core tests, strict Clippy with warnings denied, formatting, and diff checks. It is ready for your requested review against the shared validation and redaction cases.

wolfyy970 and others added 5 commits August 6, 2026 06:46
Signed-off-by: KC <79471844+wolfyy970@users.noreply.github.com>
Signed-off-by: KC <79471844+wolfyy970@users.noreply.github.com>
Signed-off-by: KC <79471844+wolfyy970@users.noreply.github.com>
Signed-off-by: KC <79471844+wolfyy970@users.noreply.github.com>
Signed-off-by: custard-pirate <custard-pirate@users.noreply.github.com>
@custard-pirate
custard-pirate force-pushed the agent/acp-http-mcp-servers branch from ddce720 to 3204b92 Compare August 6, 2026 13:24

@wolfyy970 wolfyy970 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for restacking this on #4164. The ACP HTTP wire shape is right, and the capability check keeps credentials out of session/new for unsupported adapters.

I found three security blockers and two lifecycle or observability regressions in the inline notes. There is also one composition bug: the legacy name collision check only examines Stdio, so an HTTP server can duplicate the legacy server name.

Please add a fake-adapter test that negotiates HTTP and captures the exact session/new payload. Because #4164 has not shipped, I would also fold the HTTP variant into its version 1 definition before either PR merges so version 1 never changes meaning.

Comment thread crates/buzz-acp/src/config.rs Outdated
name: &str,
description: &str,
) -> Result<String, ConfigError> {
let contents = std::fs::read_to_string(path).map_err(|error| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These credential sources are read with unbounded read_to_string. Relative paths, symlinks, FIFOs, devices, shared permissions and oversized files all pass, unlike the protected top-level config reader. An imported config could read any effective-user-readable file and send it as a URL or header. Please keep the core HTTP variant resolved (url plus header values) and resolve Project-owned selectors before the handoff. If selectors stay here, use the same bounded, no-follow, regular-file, owner-only reader on Unix and Windows.

Comment thread crates/buzz-acp/src/config.rs Outdated
"remote MCP server '{name}' has invalid URL: {error}"
))
})?;
let private_http = parsed.scheme() == "http"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This accepts bearer headers over any RFC1918 or ULA host while rejecting loopback. A private LAN is not a confidential transport. Please require HTTPS except for literal loopback, and reject credentials on cleartext loopback unless an explicit operator policy allows them.

Comment thread crates/buzz-acp/src/config.rs Outdated
"remote MCP server '{name}' header credential resolved empty or contains a control delimiter"
)));
}
if !header.value_prefix.is_empty() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The delimiter check happens before value_prefix is appended. A prefix containing CR, LF or NUL bypasses the check, and Debug prints the prefix unchanged. Validate the final composed value and redact the prefix with the credential.

Comment thread crates/buzz-acp/src/acp.rs Outdated
for value in servers
.iter()
.flat_map(McpServer::sensitive_values)
.filter(|value| !value.is_empty())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Every stdio environment value becomes an unrestricted substring pattern. Values such as 1, /, true or prod will turn unrelated errors and observer strings into [REDACTED]. That regresses existing stdio visibility. Keep structural redaction, but add a safe policy for short or common values and a regression test.

channel_id: Option<Uuid>,
channel_type: Option<&str>,
) -> Result<String, AcpError> {
validate_mcp_transport_capabilities(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The pool already knows this capability after initialize, but waits until the first session to reject the configuration. The agent can appear ready and then fail every turn. Validate each initialized slot before publishing readiness, with the same failure during lazy wakeup.

Signed-off-by: custard-pirate <custard-pirate@users.noreply.github.com>
@custard-pirate
custard-pirate force-pushed the agent/acp-http-mcp-servers branch from 70e27b7 to 749ef0c Compare August 6, 2026 15:29
@custard-pirate

Copy link
Copy Markdown
Author

@wolfyy970 Thank you for the careful review — the security, lifecycle, and composition notes were very helpful.

We addressed the findings in signed commit 749ef0c628e3e8923fe8e9bd12a74ee7f72e795c:

  • removed secondary file/environment secret selectors from the shared v1 schema; it now accepts only final resolved URLs and header values
  • require HTTPS except for credential-free literal IPv4/IPv6 loopback HTTP, rejecting RFC1918/ULA cleartext and URL userinfo or headers on loopback
  • reject empty/control-bearing final header values and redact HTTP URLs/header values from Debug, wire logs, and observer events
  • constrained fallback substring redaction so short/common values cannot corrupt unrelated diagnostics
  • validate HTTP capability during initial startup, lazy wake-up, and every respawn before the pool is marked ready
  • include HTTP entries in the legacy-name collision check
  • added a fake-adapter test asserting the exact mixed legacy-stdio plus HTTP session/new payload

Our additional deployment findings were that transport negotiation and tool approval need to remain separate fail-closed gates: unattended agents must pre-authorize only the required tools per agent/server, without a global dontAsk bypass. We also found that config parsing and model completion alone are insufficient acceptance evidence. The release gate now calls a harmless tool through both legacy stdio and structured HTTP using the real adapter, then verifies publication through constrained send. This specifically guards against the mixed-session failure mode where the legacy stdio tools were absent despite the HTTP server being configured.

Validation is clean: 257 buzz-core tests, 737 buzz-acp tests, config_env, formatting, Clippy with -D warnings, and git diff --check all pass. DCO also passes on the updated PR.

I agree that HTTP should be folded into #4164 before either PR merges so v1 never changes meaning. GitHub denied this account permission to fast-forward your fork branch (403), so I left the exact two signed commits and validation handoff on #4164: 3204b925970a38265d96bfb5a54c11736832e814 followed by 749ef0c628e3e8923fe8e9bd12a74ee7f72e795c. Once they are applied there, this PR can be closed as superseded.

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.

3 participants