Skip to content

Commit b8680be

Browse files
committed
test(http): create native MCP tests
1 parent e0686e3 commit b8680be

File tree

2 files changed

+200
-2
lines changed

2 files changed

+200
-2
lines changed

tests/http/http_mcp_test.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package http
16+
17+
import (
18+
"context"
19+
"crypto/rand"
20+
"crypto/rsa"
21+
"encoding/json"
22+
"net/http"
23+
"net/http/httptest"
24+
"regexp"
25+
"testing"
26+
"time"
27+
28+
"github.com/MicahParks/jwkset"
29+
"github.com/googleapis/genai-toolbox/internal/testutils"
30+
"github.com/googleapis/genai-toolbox/tests"
31+
)
32+
33+
func getMCPHTTPSourceConfig(t *testing.T) map[string]any {
34+
idToken, err := tests.GetGoogleIdToken(tests.ClientId)
35+
if err != nil {
36+
t.Logf("Warning: error getting ID token: %s. Using dummy token.", err)
37+
idToken = "dummy-token"
38+
}
39+
idToken = "Bearer " + idToken
40+
41+
return map[string]any{
42+
"type": HttpSourceType,
43+
"headers": map[string]string{"Authorization": idToken},
44+
}
45+
}
46+
47+
func TestHTTPListTools(t *testing.T) {
48+
// Start a test server with multiTool handler
49+
server := httptest.NewServer(http.HandlerFunc(multiTool))
50+
defer server.Close()
51+
52+
sourceConfig := getMCPHTTPSourceConfig(t)
53+
sourceConfig["baseUrl"] = server.URL
54+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
55+
defer cancel()
56+
57+
// Set up generic auth mock server (copied from legacy test)
58+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
59+
if err != nil {
60+
t.Fatalf("failed to create RSA private key: %v", err)
61+
}
62+
jwksServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
63+
if r.URL.Path == "/.well-known/openid-configuration" {
64+
w.Header().Set("Content-Type", "application/json")
65+
_ = json.NewEncoder(w).Encode(map[string]interface{}{
66+
"issuer": "https://example.com",
67+
"jwks_uri": "http://" + r.Host + "/jwks",
68+
})
69+
return
70+
}
71+
if r.URL.Path == "/jwks" {
72+
options := jwkset.JWKOptions{
73+
Metadata: jwkset.JWKMetadataOptions{
74+
KID: "test-key-id",
75+
},
76+
}
77+
jwk, _ := jwkset.NewJWKFromKey(privateKey.Public(), options)
78+
w.Header().Set("Content-Type", "application/json")
79+
_ = json.NewEncoder(w).Encode(map[string]interface{}{
80+
"keys": []jwkset.JWKMarshal{jwk.Marshal()},
81+
})
82+
return
83+
}
84+
http.NotFound(w, r)
85+
}))
86+
defer jwksServer.Close()
87+
88+
toolsFile := getHTTPToolsConfig(sourceConfig, HttpToolType, jwksServer.URL)
89+
90+
// Start the toolbox server using our NEW helper!
91+
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, "--enable-api=false")
92+
if err != nil {
93+
t.Fatalf("command initialization returned an error: %s", err)
94+
}
95+
defer cleanup()
96+
97+
// Wait for server ready
98+
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
99+
defer cancel()
100+
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
101+
if err != nil {
102+
t.Logf("toolbox command logs: \n%s", out)
103+
t.Fatalf("toolbox didn't start successfully: %s", err)
104+
}
105+
106+
// We expect the tools listed in getHTTPToolsConfig to be available!
107+
// Let's verify a subset of them to keep it simple first.
108+
expectedTools := []tests.MCPToolManifest{
109+
{
110+
Name: "my-simple-tool",
111+
Description: "Simple tool to test end to end functionality.",
112+
},
113+
{
114+
Name: "my-tool",
115+
Description: "some description",
116+
},
117+
}
118+
119+
tests.RunMCPToolsListMethod(t, expectedTools)
120+
}
121+
122+
func TestHTTPCallTool(t *testing.T) {
123+
// Start a test server with multiTool handler
124+
server := httptest.NewServer(http.HandlerFunc(multiTool))
125+
defer server.Close()
126+
127+
sourceConfig := getMCPHTTPSourceConfig(t)
128+
sourceConfig["baseUrl"] = server.URL
129+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
130+
defer cancel()
131+
132+
// Set up generic auth mock server (needed for config generation)
133+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
134+
if err != nil {
135+
t.Fatalf("failed to create RSA private key: %v", err)
136+
}
137+
jwksServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
138+
if r.URL.Path == "/.well-known/openid-configuration" {
139+
w.Header().Set("Content-Type", "application/json")
140+
_ = json.NewEncoder(w).Encode(map[string]interface{}{
141+
"issuer": "https://example.com",
142+
"jwks_uri": "http://" + r.Host + "/jwks",
143+
})
144+
return
145+
}
146+
if r.URL.Path == "/jwks" {
147+
options := jwkset.JWKOptions{
148+
Metadata: jwkset.JWKMetadataOptions{
149+
KID: "test-key-id",
150+
},
151+
}
152+
jwk, _ := jwkset.NewJWKFromKey(privateKey.Public(), options)
153+
w.Header().Set("Content-Type", "application/json")
154+
_ = json.NewEncoder(w).Encode(map[string]interface{}{
155+
"keys": []jwkset.JWKMarshal{jwk.Marshal()},
156+
})
157+
return
158+
}
159+
http.NotFound(w, r)
160+
}))
161+
defer jwksServer.Close()
162+
163+
toolsFile := getHTTPToolsConfig(sourceConfig, HttpToolType, jwksServer.URL)
164+
165+
// Start the toolbox server using our NEW helper!
166+
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, "--enable-api=false")
167+
if err != nil {
168+
t.Fatalf("command initialization returned an error: %s", err)
169+
}
170+
defer cleanup()
171+
172+
// Wait for server ready
173+
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
174+
defer cancel()
175+
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
176+
if err != nil {
177+
t.Logf("toolbox command logs: \n%s", out)
178+
t.Fatalf("toolbox didn't start successfully: %s", err)
179+
}
180+
181+
// Test calling "my-simple-tool"
182+
// From handleTool0, it expects POST and returns `"hello world"`
183+
tests.RunMCPCustomToolCallMethod(t, "my-simple-tool", map[string]any{}, `"hello world"`)
184+
}

tests/server.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"io"
2121
"os"
22+
"strings"
2223

2324
yaml "github.com/goccy/go-yaml"
2425
"github.com/spf13/cobra"
@@ -68,7 +69,21 @@ func StartCmd(ctx context.Context, toolsFile map[string]any, args ...string) (*C
6869
if err != nil {
6970
return nil, nil, fmt.Errorf("unable to write config: %s", err)
7071
}
71-
args = append(args, "--config", path, "--enable-api")
72+
73+
// Add config path
74+
args = append(args, "--config", path)
75+
76+
// Only add --enable-api if not already requested or disabled in args
77+
hasEnableApi := false
78+
for _, arg := range args {
79+
if arg == "--enable-api" || strings.HasPrefix(arg, "--enable-api=") {
80+
hasEnableApi = true
81+
break
82+
}
83+
}
84+
if !hasEnableApi {
85+
args = append(args, "--enable-api")
86+
}
7287

7388
ctx, cancel := context.WithCancel(ctx)
7489
// Open a pipe for tracking the output from the cmd
@@ -96,7 +111,6 @@ func StartCmd(ctx context.Context, toolsFile map[string]any, args ...string) (*C
96111
t.err = c.ExecuteContext(ctx)
97112
}()
98113
return t, cleanup, nil
99-
100114
}
101115

102116
// Stop sends the TERM signal to the cmd and returns.

0 commit comments

Comments
 (0)