fix(mcp): support streamable HTTP alias and request-response mode

This commit is contained in:
afjcjsbx
2026-05-07 19:24:02 +02:00
parent 81a050555d
commit 6e8590900b
22 changed files with 1032 additions and 39 deletions
+79
View File
@@ -0,0 +1,79 @@
# Integration Test Suites
This directory contains Docker-backed integration test suites that are auto-discovered by CI.
## How It Works
- The shared runner is defined in [`integration/docker-compose.runner.yml`](docker-compose.runner.yml).
- Each suite lives in `integration/suites/<suite-name>/`.
- CI and local runs use [`scripts/run-integration-tests.sh`](../scripts/run-integration-tests.sh).
- The runner discovers every suite automatically, so adding a new suite does not require editing the GitHub Actions workflow.
## Suite Layout
Each suite directory must contain:
- `suite.env`
- at least one `docker-compose.yml` or `docker-compose.*.yml`
Example:
```text
integration/suites/my-suite/
├── docker-compose.yml
└── suite.env
```
## Required Manifest Fields
`suite.env` is sourced by the runner script and must define:
- `TEST_COMMAND`: shell command executed inside the integration runner container
Optional fields:
- `RUNNER_SERVICE`: override the default runner service name (`integration-runner`)
Example:
```bash
TEST_COMMAND='go test ./pkg/mcp -run TestIntegration_RealConfiguredServer -v'
```
## Docker Conventions
Suite compose files can:
- define dependency services needed by the tests
- extend or override the shared `integration-runner` service
- inject environment variables into the runner for the tests to consume
The provided `mcp-streamable` suite is a good reference:
- it starts a local streamable HTTP MCP server
- wires the runner to that service through Docker networking
- runs the Go integration test against the containerized server
## Running Locally
Run all suites:
```bash
bash ./scripts/run-integration-tests.sh
```
Run one suite:
```bash
bash ./scripts/run-integration-tests.sh mcp-streamable
```
## Adding a New Suite
1. Create `integration/suites/<name>/docker-compose.yml`.
2. Create `integration/suites/<name>/suite.env`.
3. Put any fixture service code under `integration/fixtures/` if it is reusable.
4. Make sure the suite is self-contained and deterministic.
5. Validate it locally with `bash ./scripts/run-integration-tests.sh <name>`.
Once committed, the suite will be picked up automatically by the CI integration job.
+19
View File
@@ -0,0 +1,19 @@
services:
integration-runner:
image: golang:1.25-bookworm
working_dir: /workspace
entrypoint: ["bash", "-lc"]
volumes:
- .:/workspace
- picoclaw-integration-gocache:/go-build-cache
- picoclaw-integration-gomodcache:/go-mod-cache
environment:
GOCACHE: /go-build-cache
GOMODCACHE: /go-mod-cache
GOTOOLCHAIN: local
CGO_ENABLED: "0"
GOFLAGS: -tags=goolm,stdjson,integration
volumes:
picoclaw-integration-gocache:
picoclaw-integration-gomodcache:
@@ -0,0 +1,23 @@
FROM golang:1.25-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/mcp-streamable-server ./integration/fixtures/mcp-streamable-server
FROM alpine:3.22
RUN adduser -D -u 10001 appuser
COPY --from=build /out/mcp-streamable-server /usr/local/bin/mcp-streamable-server
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=5s --timeout=3s --retries=12 CMD wget -qO- http://127.0.0.1:8080/healthz || exit 1
ENTRYPOINT ["/usr/local/bin/mcp-streamable-server"]
@@ -0,0 +1,71 @@
package main
import (
"context"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
server := mcp.NewServer(&mcp.Implementation{
Name: "picoclaw-integration-streamable-server",
Version: "1.0.0",
}, nil)
mcp.AddTool(server, &mcp.Tool{
Name: "echo",
Description: "Echo back the provided message",
}, func(ctx context.Context, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
message, _ := args["message"].(string)
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: message},
},
}, nil, nil
})
streamable := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
return server
}, &mcp.StreamableHTTPOptions{
JSONResponse: envBool("STREAMABLE_JSON_RESPONSE", true),
})
mux := http.NewServeMux()
mux.Handle("/mcp", streamable)
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
log.Printf("streamable MCP integration server listening on %s", srv.Addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}
func envBool(name string, fallback bool) bool {
value := strings.TrimSpace(strings.ToLower(os.Getenv(name)))
if value == "" {
return fallback
}
switch value {
case "1", "true", "yes", "on":
return true
case "0", "false", "no", "off":
return false
default:
return fallback
}
}
@@ -0,0 +1,19 @@
services:
integration-runner:
depends_on:
mcp-streamable-server:
condition: service_healthy
environment:
PICOCLAW_MCP_REAL_SERVER_JSON: >-
{"enabled":true,"type":"http","url":"http://mcp-streamable-server:8080/mcp"}
PICOCLAW_MCP_REAL_TOOL_NAME: echo
PICOCLAW_MCP_REAL_TOOL_ARGS_JSON: >-
{"message":"hello from docker integration suite"}
PICOCLAW_MCP_REAL_EXPECT_SUBSTRING: hello from docker integration suite
mcp-streamable-server:
build:
context: .
dockerfile: integration/fixtures/mcp-streamable-server/Dockerfile
environment:
STREAMABLE_JSON_RESPONSE: "true"
@@ -0,0 +1 @@
TEST_COMMAND='go test ./pkg/mcp -run TestIntegration_RealConfiguredServer -v'