Skip to content

Commit f907b13

Browse files
committed
Add first conformance test (fails)
1 parent b86afc8 commit f907b13

File tree

3 files changed

+430
-14
lines changed

3 files changed

+430
-14
lines changed

conformance/conformance_test.go

+289
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
package conformance_test
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"os"
10+
"strings"
11+
"testing"
12+
13+
"github.com/docker/docker/api/types/container"
14+
"github.com/docker/docker/api/types/network"
15+
"github.com/docker/docker/client"
16+
"github.com/docker/docker/pkg/stdcopy"
17+
"github.com/google/go-cmp/cmp"
18+
"github.com/stretchr/testify/require"
19+
)
20+
21+
type maintainer string
22+
23+
const (
24+
anthropic maintainer = "anthropic"
25+
github maintainer = "github"
26+
)
27+
28+
type testLogWriter struct {
29+
t *testing.T
30+
}
31+
32+
func (w testLogWriter) Write(p []byte) (n int, err error) {
33+
w.t.Log(string(p))
34+
return len(p), nil
35+
}
36+
37+
func start(t *testing.T, m maintainer) server {
38+
var image string
39+
if m == github {
40+
image = "github/github-mcp-server"
41+
} else {
42+
image = "mcp/github"
43+
}
44+
45+
ctx := context.Background()
46+
dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
47+
require.NoError(t, err)
48+
49+
containerCfg := &container.Config{
50+
OpenStdin: true,
51+
AttachStdin: true,
52+
AttachStdout: true,
53+
AttachStderr: true,
54+
Env: []string{
55+
fmt.Sprintf("GITHUB_PERSONAL_ACCESS_TOKEN=%s", os.Getenv("GITHUB_PERSONAL_ACCESS_TOKEN")),
56+
},
57+
Image: image,
58+
}
59+
60+
resp, err := dockerClient.ContainerCreate(
61+
ctx,
62+
containerCfg,
63+
&container.HostConfig{},
64+
&network.NetworkingConfig{},
65+
nil,
66+
"")
67+
require.NoError(t, err)
68+
69+
t.Cleanup(func() {
70+
require.NoError(t, dockerClient.ContainerRemove(ctx, resp.ID, container.RemoveOptions{Force: true}))
71+
})
72+
73+
hijackedResponse, err := dockerClient.ContainerAttach(ctx, resp.ID, container.AttachOptions{
74+
Stream: true,
75+
Stdin: true,
76+
Stdout: true,
77+
Stderr: true,
78+
})
79+
require.NoError(t, err)
80+
t.Cleanup(func() { hijackedResponse.Close() })
81+
82+
require.NoError(t, dockerClient.ContainerStart(ctx, resp.ID, container.StartOptions{}))
83+
84+
serverStart := make(chan serverStartResult)
85+
go func() {
86+
prOut, pwOut := io.Pipe()
87+
prErr, pwErr := io.Pipe()
88+
89+
go func() {
90+
// Ignore error, we should be done?
91+
// TODO: maybe check for use of closed network connection specifically
92+
_, _ = stdcopy.StdCopy(pwOut, pwErr, hijackedResponse.Reader)
93+
pwOut.Close()
94+
pwErr.Close()
95+
}()
96+
97+
bufferedStderr := bufio.NewReader(prErr)
98+
line, err := bufferedStderr.ReadString('\n')
99+
if err != nil {
100+
serverStart <- serverStartResult{err: err}
101+
}
102+
103+
if strings.TrimSpace(line) != "GitHub MCP Server running on stdio" {
104+
serverStart <- serverStartResult{
105+
err: fmt.Errorf("unexpected server output: %s", line),
106+
}
107+
return
108+
}
109+
110+
serverStart <- serverStartResult{
111+
server: server{
112+
m: m,
113+
log: testLogWriter{t},
114+
stdin: hijackedResponse.Conn,
115+
stdout: bufio.NewReader(prOut),
116+
},
117+
}
118+
}()
119+
120+
t.Logf("waiting for %s server to start...", m)
121+
serveResult := <-serverStart
122+
require.NoError(t, serveResult.err, "expected the server to start successfully")
123+
124+
return serveResult.server
125+
}
126+
127+
func TestCapabilities(t *testing.T) {
128+
githubServer := start(t, github)
129+
anthropicServer := start(t, anthropic)
130+
131+
req := newInitializeRequest(
132+
initializeRequestParams{
133+
ProtocolVersion: "2024-11-05",
134+
Capabilities: clientCapabilities{},
135+
ClientInfo: clientInfo{
136+
Name: "ConformanceTest",
137+
Version: "0.0.1",
138+
},
139+
},
140+
)
141+
142+
require.NoError(t, githubServer.send(req))
143+
144+
var ghInitializeResponse initializeResponse
145+
require.NoError(t, githubServer.receive(&ghInitializeResponse))
146+
147+
require.NoError(t, anthropicServer.send(req))
148+
149+
var anthropicInitializeResponse initializeResponse
150+
require.NoError(t, anthropicServer.receive(&anthropicInitializeResponse))
151+
152+
if diff := cmp.Diff(ghInitializeResponse.Result.Capabilities, anthropicInitializeResponse.Result.Capabilities); diff != "" {
153+
t.Errorf("unexpected capability differential: %s", diff)
154+
}
155+
}
156+
157+
type serverStartResult struct {
158+
server server
159+
err error
160+
}
161+
162+
type server struct {
163+
m maintainer
164+
log io.Writer
165+
166+
stdin io.Writer
167+
stdout *bufio.Reader
168+
}
169+
170+
func (s server) send(req request) error {
171+
b, err := req.marshal()
172+
if err != nil {
173+
return err
174+
}
175+
176+
fmt.Fprintf(s.log, "sending %s: %s\n", s.m, string(b))
177+
178+
n, err := s.stdin.Write(append(b, '\n'))
179+
if err != nil {
180+
return err
181+
}
182+
183+
if n != len(b)+1 {
184+
return fmt.Errorf("wrote %d bytes, expected %d", n, len(b)+1)
185+
}
186+
187+
return nil
188+
}
189+
190+
func (s server) receive(res response) error {
191+
line, err := s.stdout.ReadBytes('\n')
192+
if err != nil {
193+
if err == io.EOF {
194+
return fmt.Errorf("EOF after reading %s", string(line))
195+
}
196+
return err
197+
}
198+
199+
fmt.Fprintf(s.log, "received from %s: %s\n", s.m, string(line))
200+
201+
return res.unmarshal(line)
202+
}
203+
204+
type jsonRPRCRequest[params any] struct {
205+
JSONRPC string `json:"jsonrpc"`
206+
ID int `json:"id"`
207+
Method string `json:"method"`
208+
Params params `json:"params"`
209+
}
210+
211+
type jsonRPRCResponse[result any] struct {
212+
JSONRPC string `json:"jsonrpc"`
213+
ID int `json:"id"`
214+
Method string `json:"method"`
215+
Result result `json:"result"`
216+
}
217+
218+
type request interface {
219+
marshal() ([]byte, error)
220+
}
221+
222+
type response interface {
223+
unmarshal([]byte) error
224+
}
225+
226+
func newInitializeRequest(params initializeRequestParams) initializeRequest {
227+
return initializeRequest{
228+
jsonRPRCRequest: jsonRPRCRequest[initializeRequestParams]{
229+
JSONRPC: "2.0",
230+
ID: 1,
231+
Method: "initialize",
232+
Params: params,
233+
},
234+
}
235+
}
236+
237+
type initializeRequest struct {
238+
jsonRPRCRequest[initializeRequestParams]
239+
}
240+
241+
func (r initializeRequest) marshal() ([]byte, error) {
242+
return json.Marshal(r)
243+
}
244+
245+
type initializeRequestParams struct {
246+
ProtocolVersion string `json:"protocolVersion"`
247+
Capabilities clientCapabilities `json:"capabilities"`
248+
ClientInfo clientInfo `json:"clientInfo"`
249+
}
250+
251+
type clientCapabilities struct{} // don't actually care about any of these right now
252+
253+
type clientInfo struct {
254+
Name string `json:"name"`
255+
Version string `json:"version"`
256+
}
257+
258+
type initializeResponse struct {
259+
jsonRPRCResponse[initializeResult]
260+
}
261+
262+
func (r *initializeResponse) unmarshal(b []byte) error {
263+
return json.Unmarshal(b, r)
264+
}
265+
266+
type initializeResult struct {
267+
ProtocolVersion string `json:"protocolVersion"`
268+
Capabilities serverCapabilities `json:"capabilities"`
269+
ServerInfo serverInfo `json:"serverInfo"`
270+
}
271+
272+
type serverCapabilities struct {
273+
Logging struct{} `json:"logging"`
274+
Prompts struct {
275+
ListChanged bool `json:"listChanged"`
276+
} `json:"prompts"`
277+
Resources struct {
278+
Subscribe bool `json:"subscribe"`
279+
ListChanged bool `json:"listChanged"`
280+
} `json:"resources"`
281+
Tools struct {
282+
ListChanged bool `json:"listChanged"`
283+
} `json:"tools"`
284+
}
285+
286+
type serverInfo struct {
287+
Name string `json:"name"`
288+
Version string `json:"version"`
289+
}

go.mod

+31-3
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,45 @@ go 1.23.7
44

55
require (
66
github.com/aws/smithy-go v1.22.3
7+
github.com/docker/docker v28.0.4+incompatible
8+
github.com/google/go-cmp v0.7.0
79
github.com/google/go-github/v69 v69.2.0
810
github.com/mark3labs/mcp-go v0.14.1
911
github.com/migueleliasweb/go-github-mock v1.1.0
1012
github.com/sirupsen/logrus v1.9.3
1113
github.com/spf13/cobra v1.9.1
1214
github.com/spf13/viper v1.19.0
13-
github.com/stretchr/testify v1.9.0
15+
github.com/stretchr/testify v1.10.0
1416
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
1517
)
1618

1719
require (
20+
github.com/Microsoft/go-winio v0.6.2 // indirect
21+
github.com/containerd/log v0.1.0 // indirect
1822
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
23+
github.com/distribution/reference v0.6.0 // indirect
24+
github.com/docker/go-connections v0.5.0 // indirect
25+
github.com/docker/go-units v0.5.0 // indirect
26+
github.com/felixge/httpsnoop v1.0.4 // indirect
1927
github.com/fsnotify/fsnotify v1.7.0 // indirect
28+
github.com/go-logr/logr v1.4.2 // indirect
29+
github.com/go-logr/stdr v1.2.2 // indirect
30+
github.com/gogo/protobuf v1.3.2 // indirect
2031
github.com/google/go-github/v64 v64.0.0 // indirect
2132
github.com/google/go-querystring v1.1.0 // indirect
2233
github.com/google/uuid v1.6.0 // indirect
2334
github.com/gorilla/mux v1.8.0 // indirect
2435
github.com/hashicorp/hcl v1.0.0 // indirect
2536
github.com/inconshreveable/mousetrap v1.1.0 // indirect
26-
github.com/magiconair/properties v1.8.7 // indirect
37+
github.com/magiconair/properties v1.8.9 // indirect
2738
github.com/mitchellh/mapstructure v1.5.0 // indirect
39+
github.com/moby/docker-image-spec v1.3.1 // indirect
40+
github.com/moby/term v0.5.0 // indirect
41+
github.com/morikuni/aec v1.0.0 // indirect
42+
github.com/opencontainers/go-digest v1.0.0 // indirect
43+
github.com/opencontainers/image-spec v1.1.1 // indirect
2844
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
45+
github.com/pkg/errors v0.9.1 // indirect
2946
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
3047
github.com/sagikazarmark/locafero v0.4.0 // indirect
3148
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
@@ -35,11 +52,22 @@ require (
3552
github.com/spf13/pflag v1.0.6 // indirect
3653
github.com/subosito/gotenv v1.6.0 // indirect
3754
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
55+
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
56+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
57+
go.opentelemetry.io/otel v1.35.0 // indirect
58+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
59+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
60+
go.opentelemetry.io/otel/metric v1.35.0 // indirect
61+
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
62+
go.opentelemetry.io/otel/trace v1.35.0 // indirect
63+
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
3864
go.uber.org/atomic v1.9.0 // indirect
3965
go.uber.org/multierr v1.9.0 // indirect
40-
golang.org/x/sys v0.28.0 // indirect
66+
golang.org/x/sys v0.31.0 // indirect
4167
golang.org/x/text v0.21.0 // indirect
4268
golang.org/x/time v0.5.0 // indirect
69+
google.golang.org/protobuf v1.36.5 // indirect
4370
gopkg.in/ini.v1 v1.67.0 // indirect
4471
gopkg.in/yaml.v3 v3.0.1 // indirect
72+
gotest.tools/v3 v3.5.1 // indirect
4573
)

0 commit comments

Comments
 (0)