Skip to content

feat(control-plane): embedded MCP server at /mcp (zero-setup harness integration)#817

Open
AbirAbbas wants to merge 5 commits into
mainfrom
feat/control-plane-mcp-server
Open

feat(control-plane): embedded MCP server at /mcp (zero-setup harness integration)#817
AbirAbbas wants to merge 5 commits into
mainfrom
feat/control-plane-mcp-server

Conversation

@AbirAbbas

Copy link
Copy Markdown
Contributor

What & why

AgentField had no MCP server — the only way for an AI harness to drive installed
agents was the af CLI plus the agentfield-use skill. This PR ships an
MCP server embedded in the control plane itself, served on the same port as
the REST API
. Whether the control plane was started by the CLI (af server)
or the desktop app, MCP is just there — zero extra processes, zero setup. A
harness connects with one command:

claude mcp add --transport http agentfield http://localhost:8080/mcp

It is on by default, with an env kill-switch: AGENTFIELD_MCP_ENABLED=false
makes /mcp return 404. The endpoint sits behind the same global API-key auth
and trust domain as /api/v1, and the tools call the existing discovery /
execution service layer directly (no loopback HTTP).

  • Transport: streamable HTTP, JSON-RPC 2.0 over POST /mcp (stateless;
    Mcp-Session-Id / MCP-Protocol-Version headers tolerated and ignored).
    GET /mcp → 405, OPTIONS /mcp → 204.

The five tools

Tool Purpose
discover_agents List agents + reasoners (id, description, entrypoint tags). Defaults to active-only; health:"all" includes unhealthy.
get_reasoner_schema Input/output JSON Schema for a reasoner, as served by discovery.
execute_reasoner Validate node.reasoner exists, start an async run, return {run_id, status:"accepted"}.
get_run Run status + result/error + per-execution summaries.
wait_run Server-side poll to terminal or timeout (default 60s, hard-capped at 120s); adds a timed_out flag.

Validation Contract → tests

  • initialize returns protocolVersion + capabilities.tools + serverInfo →
    TestMCP_InitializeAndToolsList (also asserts unknown protocol falls back to
    2025-06-18 and serverInfo carries the build version).
  • tools/list returns exactly the 5 tools, each with a valid JSON Schema →
    TestMCP_InitializeAndToolsList.
  • discover_agents default returns only active agents; health:"all" includes
    dead ones with health_statusTestMCP_DiscoverAgents.
  • execute_reasoner bad/malformed target → tool error naming discover_agents;
    good target → run_id and a created execution → TestMCP_ExecuteReasoner.
  • get_run aggregates status/result/error + per-execution summaries →
    TestMCP_GetRun.
  • wait_run respects the timeout cap and returns timed_out on a never-finishing
    run (and returns immediately for a terminal run) → TestMCP_WaitRun.
  • Unknown method → -32601; unknown tool → -32602; batch arrays rejected →
    TestMCP_ProtocolMiscMethods.
  • AGENTFIELD_MCP_ENABLED=false/mcp is 404; default → enabled;
    GET /mcp → 405 → TestMCPRoutes_DisabledReturns404,
    TestMCPRoutes_EnabledByDefault.
  • Config default-on + env override → TestMCPConfig_IsEnabledDefault,
    TestApplyEnvOverrides_MCPEnabled.

Test results

  • go build ./... — clean
  • go vet ./internal/config/... ./internal/handlers/... ./internal/server/... — clean
  • go test for the config, handlers, server (MCP) and skillkit packages — all pass
  • gofmt -l on changed files — empty
  • golangci-lint — no issues in any changed/new file

One pre-existing, environment-specific test (TestStartAndStopCoverAdditionalBranches,
admin gRPC listener bind) fails identically on clean origin/main in this sandbox
and is unrelated to this change.

🤖 Generated with Claude Code

AbirAbbas and others added 5 commits July 22, 2026 15:33
Introduce MCPConfig under features with an IsEnabled() default of true so
the embedded MCP server ships on by default. AGENTFIELD_MCP_ENABLED=false
flips it off via the existing env-override precedence path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a stateless streamable-HTTP JSON-RPC 2.0 handler that exposes AgentField
discovery and execution as MCP tools, calling the existing service layer
directly (no loopback HTTP). Supports initialize, notifications, ping,
tools/list and tools/call; rejects batch arrays; unknown methods return
-32601.

Five tools: discover_agents, get_reasoner_schema, execute_reasoner (starts an
async run), get_run, and wait_run (server-side poll with a hard timeout cap so
a tool call can never hang a harness). Results are single compact-JSON text
content blocks; validation/business failures come back as isError tool
results rather than transport errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register the embedded MCP server on the same Gin router as the REST API:
POST /mcp (JSON-RPC), GET /mcp -> 405, OPTIONS /mcp -> 204. The route is
gated by AGENTFIELD_MCP_ENABLED and simply not registered (so /mcp 404s) when
disabled. It lives behind the same global API-key auth and trust domain as
/api/v1 — no extra process, no extra setup; harnesses connect with one
command.

Thread the build version through server.SetBuildVersion so the MCP
serverInfo reports the real control-plane version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add an "MCP (zero-setup)" section near the top of the agentfield-use skill
(both the repo copy and the embedded skill_data copy, kept identical): the
control plane serves MCP at <server>/mcp, with the one-liner claude mcp add
command and a generic streamable-HTTP note for other clients. The CLI/REST
flow remains the documented full-power path. Bump the skill version to 0.4.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add docs/mcp-integration.md covering the endpoint, the five tools, the
security posture (same trust domain and API-key auth as the REST API), the
disable flag, and an example tool-call flow. Link it from the README feature
table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AbirAbbas
AbirAbbas requested a review from a team as a code owner July 22, 2026 19:34
@github-actions

Copy link
Copy Markdown
Contributor

📊 Coverage gate

Thresholds from .coverage-gate.toml: per-surface ≥ 84%, aggregate ≥ 85%, max per-surface regression ≤ 1.0 pp, max aggregate regression ≤ 0.50 pp.

Surface Current Baseline Δ
control-plane 86.80% 87.40% ↓ -0.60 pp 🟡
sdk-go 92.40% 92.00% ↑ +0.40 pp 🟢
sdk-python 93.82% 93.73% ↑ +0.09 pp 🟢
sdk-typescript 91.05% 90.42% ↑ +0.63 pp 🟢
web-ui 84.76% 84.79% ↓ -0.03 pp 🟡
aggregate 85.51% 85.75% ↓ -0.24 pp 🟡

✅ Gate passed

No surface regressed past the allowed threshold and the aggregate stayed above the floor.

@github-actions

Copy link
Copy Markdown
Contributor

📐 Patch coverage gate

Threshold: 80% on lines this PR touches vs origin/main (from .coverage-gate.toml:thresholds.min_patch).

Surface Touched lines Patch coverage Status
control-plane 578 84.00%
sdk-go 0 ➖ no changes
sdk-python 0 ➖ no changes
sdk-typescript 0 ➖ no changes
web-ui 0 ➖ no changes

✅ Patch gate passed

Every surface whose lines were touched by this PR has patch coverage at or above the threshold.

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