Skip to content

feat: add Control MCP server for introspecting process-compose itself#467

Open
jeanlucthumm wants to merge 17 commits intoF1bonacc1:mainfrom
jeanlucthumm:jeanlucthumm-mcp-orchestration
Open

feat: add Control MCP server for introspecting process-compose itself#467
jeanlucthumm wants to merge 17 commits intoF1bonacc1:mainfrom
jeanlucthumm:jeanlucthumm-mcp-orchestration

Conversation

@jeanlucthumm
Copy link
Copy Markdown

Summary

Adds an optional Control MCP server (src/mcpctl/) that exposes 8 MCP
tools letting agents introspect and control the running process-compose
instance itself — BM25-ranked log search, list/get processes, tail recent
logs, start/stop/restart processes, and read the dependency graph.

Closes #460.

Context

This is orthogonal to the existing src/mcp/ package (which wraps
user-defined processes as MCP tools). Both packages use
mark3labs/mcp-go but share no Go types, interfaces, or config — they
run as two independent servers on separate ports and can be enabled
independently.

Usage

Enable via a new top-level block in the project YAML:

mcpctl_server:
  host: localhost
  port: 11001

With the above, running process-compose up exposes an SSE MCP endpoint
at http://localhost:11001/sse. Off by default — an absent or empty
block does nothing.

Tools

Tool Purpose
search_logs BM25-ranked search across one or all processes
list_processes Snapshot of all processes + live state
get_process Detail for one process
get_logs Tail N lines from one process
start_process / stop_process / restart_process Lifecycle
get_dependency_graph Dependency graph + live status overlay

Design notes

  • Zero new dependencies. mark3labs/mcp-go was already pinned. BM25
    is a ~160-line hand roll (src/mcpctl/bm25.go) — there's no lightweight
    maintained BM25 package in the Go ecosystem, and pulling in a full
    search engine like bleve for a few thousand log lines felt like too
    much. Rationale is documented at the top of bm25.go.
  • Independent package. No imports of src/mcp/;
    mcpctl.ProcessRunner is a fresh local interface duck-typed against
    *app.ProjectRunner.
  • SSE only. Under up, stdio is already owned by the TUI or the
    sibling mcp_server stdio transport, so mcpctl-over-stdio has no place
    to send its traffic.
  • Loopback warning. The endpoint exposes unauthenticated process
    control (same posture as the existing mcp_server). Startup logs a
    prominent warning when bound to a non-loopback address.

Files

  • New: src/mcpctl/{server,manager,tools,bm25}.go + tests
  • New: src/types/mcpctl.go + tests (MCPCtlServerConfig)
  • Modified: src/cmd/root.go (start/stop alongside mcp manager,
    unwind cleanly on partial-startup errors),
    src/loader/validators.go (validate block),
    src/types/project.go (add field),
    CLAUDE.md (package table)

Test plan

  • go test -race ./src/mcpctl/... ./src/types/... — BM25 ranking,
    Validate, Manager nil-safety, dep-graph overlay round-trip
  • make lint clean
  • make build green
  • End-to-end smoke: compose file with two processes +
    mcpctl_server block; MCP initialize, tools/list, and each
    tool call returns correctly shaped JSON-RPC responses over SSE
  • Review for upstream style / naming preferences

Out of scope (follow-ups)

  • Auth on the SSE endpoint (existing mcp_server doesn't auth either;
    the loopback warning documents the expectation)
  • CLI flag enablement — YAML-only for MVP
  • MCP resources for log streams (tools-only for first cut)
  • Stemming / fuzzy matching in search_logs

Introduces a new src/mcpctl package that exposes 8 MCP tools for agents to
introspect and control the running process-compose instance itself:
list_processes, get_process, get_logs, search_logs, start_process,
stop_process, restart_process, get_dependency_graph.

This is independent from src/mcp — that package wraps user-defined processes
as MCP tools; this one targets process-compose's own runner API. The two
packages share no types or interfaces.

Includes a hand-rolled Okapi BM25 implementation for search_logs ranking
(zero new go.mod deps).

Not yet wired into cmd/root.go; next commit enables via the new
mcpctl_server YAML block.
Load MCPCtlServer from the project YAML and start/stop it alongside the
existing MCP server in waitForProjectAndServer. Add validation so invalid
mcpctl_server blocks fail (or warn) on project load.

With this commit, a compose.yaml containing

  mcpctl_server:
    host: localhost
    port: 11001

exposes the 8 Control MCP tools over SSE when the project is up.
- Remove unused ProcessRunner methods (GetProcessInfo, GetProcessLogLength)
- Drop unused Manager.runner field
- Replace hand-rolled depNodeJSON/depLink/toDepNodeJSON/toDepLink with direct
  serialization of types.DependencyGraph; its JSON tags already produce the
  required wire format
- get_dependency_graph: batch-fetch state via GetProcessesState instead of
  N+1 GetProcessState calls
- SSE: retain the SSEServer handle and call Shutdown on Stop so the listener
  goroutine unwinds cleanly
- Introduce transport constants in both types/mcpctl.go and mcpctl/server.go
  in place of bare "sse"/"stdio" literals
- Trim verbose doc comments that narrated the design rather than the code
Under `up`, stdio is already owned by the TUI or the sibling mcp_server
stdio transport — there's no practical way to route mcpctl traffic there.
Keeping the stdio code path created a claimed-but-broken surface (stdio
was listed as a transport but SetStdio was never wired in cmd/root.go).

- Remove startStdio / SetStdio / stdin / stdout / IsStdio
- Validate() now rejects any non-SSE transport with a clear error
- IsEnabled() documents the "empty block ⇒ disabled" semantic
Declared, YAML-parsed, but never read anywhere. Users would have thought
they'd configured something. Drop the field and GetTimeout() with it.
sseServer.Start returns http.ErrServerClosed when Shutdown is called;
that's the expected path, not an error worth logging.
Previously, if mcpctl failed to bind its port after mcp had already started,
the mcp server leaked because waitForProjectAndServer returned the error
immediately without stopping it.
The returned index is the position within the log chunk we fetched for
search (tail of log_limit lines), not an absolute position in the process
log buffer — lines may roll out between calls. Rename and document.

Also drop the hardcoded "8" in the registerBuiltinTools comment and log
message; the count would drift silently as tools are added or removed.
- NewServer now copies types.Processes into an internal map so
  get_dependency_graph iterations can't race any runtime mutation of the
  project's process set. Matches the sibling mcp.Server pattern.
- get_dependency_graph now surfaces GetProcessesState errors instead of
  silently returning a graph with Pending/- placeholders. Matches the
  error policy of list_processes.
The SSE endpoint exposes full process control (start/stop/restart + logs)
with no authentication. Binding to anything other than loopback without
understanding that risk is probably a mistake — log a prominent warning
at startup instead of silently listening.
… overlay

- types/mcpctl_test.go: IsEnabled (nil/empty/populated) and Validate
  branches (stdio rejected, unknown transport rejected, missing host/port)
- mcpctl/server_test.go: nil config ⇒ nil manager (with nil-safe Start/Stop),
  populated config ⇒ non-nil; toolGetDependencyGraph round-trip asserting
  that AllNodes ⇄ Nodes pointer aliasing overlays live status onto the
  JSON result
Use the descriptions from the TS prototype (repl-it-web PR #74337) so the
agent-facing surface matches what was validated in prior use. Notable
changes:

- search_logs gains the "NOT by time — for recent/latest use get_logs"
  disambiguation and usage examples
- list_processes, get_logs, get_process gain "Use this for X" guidance
- get_dependency_graph mentions startup order / failure cascading
- name parameters include e.g. hints
With stdio gone, Transport only accepts "sse" / "" — it's a field that
exists but can't change behavior. Remove it along with IsSSE() and the
transport constant. Host + Port is now the full config surface.
@jeanlucthumm jeanlucthumm marked this pull request as ready for review April 21, 2026 19:44
@sonarqubecloud
Copy link
Copy Markdown

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.

Question about MCP

1 participant