Compare commits
2 Commits
v0.5.0
...
9bdf00f51f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bdf00f51f | ||
|
|
7f7524c859 |
@@ -1,10 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"supervisor": {
|
||||
"command": "/Users/mathias/dev/AI/supervisor/bin/supervisor-bridge",
|
||||
"env": {
|
||||
"SUPERVISOR_URL": "http://koala:30320/mcp"
|
||||
}
|
||||
"type": "http",
|
||||
"url": "http://koala:30320/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
README.md
16
README.md
@@ -10,9 +10,9 @@ into a searchable brain.
|
||||
```
|
||||
Your Claude Code session (in any project)
|
||||
│
|
||||
│ MCP tools (over stdio bridge → HTTP)
|
||||
│ MCP over HTTP (Tailscale)
|
||||
▼
|
||||
supervisor :3200 — skill workers: tdd, retrospective
|
||||
supervisor :3200 (NodePort 30320 on koala) — skill workers: tdd, retrospective
|
||||
ingestion :3300 — brain HTTP API: query wiki, write notes
|
||||
│
|
||||
▼
|
||||
@@ -55,18 +55,18 @@ Create `.mcp.json` in your project root:
|
||||
{
|
||||
"mcpServers": {
|
||||
"supervisor": {
|
||||
"command": "/Users/mathias/dev/AI/supervisor/bin/supervisor-bridge",
|
||||
"env": {
|
||||
"SUPERVISOR_URL": "http://localhost:3200/mcp"
|
||||
}
|
||||
"type": "http",
|
||||
"url": "http://koala:30320/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Build the bridge binary once: `task bridge:build`
|
||||
The supervisor MCP server is reachable over Tailscale at `koala:30320` (NodePort
|
||||
to the in-cluster service on port 3200). No local binary or stdio shim is
|
||||
required — Claude Code talks to it directly via HTTP.
|
||||
|
||||
Then open Claude Code in your project — run `/mcp` to confirm `supervisor` is listed.
|
||||
Open Claude Code in your project — run `/mcp` to confirm `supervisor` is listed.
|
||||
|
||||
## A typical TDD session
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ tasks:
|
||||
desc: Build all binaries
|
||||
cmds:
|
||||
- task: supervisor:build
|
||||
- task: bridge:build
|
||||
- task: ingestion:build
|
||||
|
||||
supervisor:build:
|
||||
@@ -65,11 +64,6 @@ tasks:
|
||||
cmds:
|
||||
- go build -trimpath -ldflags="-s -w -X main.version={{.VERSION}}" -o bin/supervisor ./cmd/supervisor
|
||||
|
||||
bridge:build:
|
||||
desc: Build stdio↔HTTP bridge for Claude Code MCP integration
|
||||
cmds:
|
||||
- go build -trimpath -ldflags="-s -w" -o bin/supervisor-bridge ./cmd/bridge
|
||||
|
||||
ingestion:build:
|
||||
desc: Build ingestion server binary
|
||||
dir: ingestion
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
// bridge is a stdio↔HTTP adapter that lets Claude Code connect to the
|
||||
// supervisor MCP server via the stdio transport.
|
||||
//
|
||||
// Claude Code spawns this binary as a subprocess and communicates over
|
||||
// stdin/stdout. Each newline-delimited JSON-RPC message from stdin is
|
||||
// forwarded to the supervisor HTTP server and the response is written back.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// SUPERVISOR_URL=http://localhost:3200/mcp bridge
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
url := os.Getenv("SUPERVISOR_URL")
|
||||
if url == "" {
|
||||
url = "http://localhost:3200/mcp"
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
if len(bytes.TrimSpace(line)) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(line))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "bridge: build request: %v\n", err)
|
||||
continue
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "bridge: request failed: %v\n", err)
|
||||
continue
|
||||
}
|
||||
_, _ = io.Copy(os.Stdout, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
_, _ = os.Stdout.Write([]byte("\n"))
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "bridge: scanner: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// JSON-RPC 2.0 notifications (no id) must not receive a response.
|
||||
if req.ID == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var result any
|
||||
var rpcErr *rpcError
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mathiasbq/supervisor/internal/mcp"
|
||||
@@ -76,3 +77,39 @@ func TestMCPUnknownMethod(t *testing.T) {
|
||||
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
|
||||
assert.NotNil(t, resp["error"])
|
||||
}
|
||||
|
||||
func TestMCPNotificationKnownMethodGetsNoResponseBody(t *testing.T) {
|
||||
reg := registry.New()
|
||||
srv := mcp.NewServer(reg)
|
||||
|
||||
// JSON-RPC 2.0 notification: "id" field absent. Per spec, server MUST NOT
|
||||
// reply. notifications/initialized is part of the standard MCP handshake.
|
||||
req := httptest.NewRequest(http.MethodPost, "/mcp", jsonBody(t, map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/initialized",
|
||||
}))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rr := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Empty(t, strings.TrimSpace(rr.Body.String()),
|
||||
"notifications must not receive a response body")
|
||||
}
|
||||
|
||||
func TestMCPNotificationUnknownMethodGetsNoResponseBody(t *testing.T) {
|
||||
reg := registry.New()
|
||||
srv := mcp.NewServer(reg)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/mcp", jsonBody(t, map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/totally-unknown",
|
||||
}))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rr := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Empty(t, strings.TrimSpace(rr.Body.String()),
|
||||
"unknown notifications must also receive no response body")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user