Skip to content

Commit 8e70cd3

Browse files
feat: Add MCP Transport version 2025-06-18 (#132)
* chore: Add Transport interface and Toolbox Transport * increase code coverage * add tests for types * linter fix * add build tags * add static check * review changes * update header to 2026 * fix test * fix tests * change tokensources to resolved headers * minor fix * feat: Add MCP Transport version 2024-11-05 * add headers * increase code coverage * seperate constants * minor fix * Add strict validation and increase code coverage * allow 202/204 status code for notifications * standardize naming convention and docstring * use version.txt * add version.go in mcp package * fix tests * add headers * review changes * add build tag * review changes and change headers to 2026 * fix tests * better error * fix tests * change tokensources into resolved strings * minor fix * chore(deps): update all non-major dependencies (#127) * feat: Add MCP Transport version 2025-03-26 * undo * allow 202/204 status codes for notifications * fetch mcp session id from header * refactor * minor fix * fix tests * code comment * better error * better error * fix tests * fetch session id through header * change tokensources into resolved strings * chore(deps): update module google.golang.org/adk to v0.3.0 (#130) * feat: Add MCP Transport version 2025-06-18 * code comment * better error * fix tests * add accept header * change tokensources into resolved strings --------- Co-authored-by: Mend Renovate <bot@renovateapp.com>
1 parent dba6000 commit 8e70cd3

3 files changed

Lines changed: 908 additions & 0 deletions

File tree

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
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 v20250618
16+
17+
import (
18+
"bytes"
19+
"context"
20+
"encoding/json"
21+
"fmt"
22+
"io"
23+
"net/http"
24+
"net/url"
25+
"strings"
26+
27+
"github.com/google/uuid"
28+
"github.com/googleapis/mcp-toolbox-sdk-go/core/transport"
29+
"github.com/googleapis/mcp-toolbox-sdk-go/core/transport/mcp"
30+
)
31+
32+
const (
33+
ProtocolVersion = "2025-06-18"
34+
ClientName = "toolbox-go-sdk"
35+
ClientVersion = mcp.SDKVersion
36+
)
37+
38+
// Ensure that McpTransport implements the Transport interface.
39+
var _ transport.Transport = &McpTransport{}
40+
41+
// McpTransport implements the MCP v2025-06-18 protocol.
42+
type McpTransport struct {
43+
*mcp.BaseMcpTransport
44+
protocolVersion string
45+
}
46+
47+
// New creates a new version-specific transport instance.
48+
func New(baseURL string, client *http.Client) (*McpTransport, error) {
49+
baseTransport, err := mcp.NewBaseTransport(baseURL, client)
50+
if err != nil {
51+
return nil, err
52+
}
53+
t := &McpTransport{
54+
BaseMcpTransport: baseTransport,
55+
protocolVersion: ProtocolVersion,
56+
}
57+
t.BaseMcpTransport.HandshakeHook = t.initializeSession
58+
59+
return t, nil
60+
}
61+
62+
// ListTools fetches available tools
63+
func (t *McpTransport) ListTools(ctx context.Context, toolsetName string, headers map[string]string) (*transport.ManifestSchema, error) {
64+
if err := t.EnsureInitialized(ctx); err != nil {
65+
return nil, err
66+
}
67+
68+
requestURL := t.BaseURL()
69+
if toolsetName != "" {
70+
var err error
71+
requestURL, err = url.JoinPath(requestURL, toolsetName)
72+
if err != nil {
73+
return nil, fmt.Errorf("failed to construct toolset URL: %w", err)
74+
}
75+
}
76+
77+
var result listToolsResult
78+
if err := t.sendRequest(ctx, requestURL, "tools/list", map[string]any{}, headers, &result); err != nil {
79+
return nil, fmt.Errorf("failed to list tools: %w", err)
80+
}
81+
82+
manifest := &transport.ManifestSchema{
83+
ServerVersion: t.ServerVersion,
84+
Tools: make(map[string]transport.ToolSchema),
85+
}
86+
87+
for i, tool := range result.Tools {
88+
if tool.Name == "" {
89+
return nil, fmt.Errorf("received invalid tool definition at index %d: missing 'name' field", i)
90+
}
91+
92+
rawTool := map[string]any{
93+
"name": tool.Name,
94+
"description": tool.Description,
95+
"inputSchema": tool.InputSchema,
96+
}
97+
if tool.Meta != nil {
98+
rawTool["_meta"] = tool.Meta
99+
}
100+
101+
toolSchema, err := t.ConvertToolDefinition(rawTool)
102+
if err != nil {
103+
return nil, fmt.Errorf("failed to convert schema for tool %s: %w", tool.Name, err)
104+
}
105+
106+
manifest.Tools[tool.Name] = toolSchema
107+
}
108+
109+
return manifest, nil
110+
}
111+
112+
// GetTool fetches a single tool
113+
func (t *McpTransport) GetTool(ctx context.Context, toolName string, headers map[string]string) (*transport.ManifestSchema, error) {
114+
manifest, err := t.ListTools(ctx, "", headers)
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
tool, exists := manifest.Tools[toolName]
120+
if !exists {
121+
return nil, fmt.Errorf("tool '%s' not found", toolName)
122+
}
123+
124+
return &transport.ManifestSchema{
125+
ServerVersion: manifest.ServerVersion,
126+
Tools: map[string]transport.ToolSchema{toolName: tool},
127+
}, nil
128+
}
129+
130+
// InvokeTool executes a tool
131+
func (t *McpTransport) InvokeTool(ctx context.Context, toolName string, payload map[string]any, headers map[string]string) (any, error) {
132+
if err := t.EnsureInitialized(ctx); err != nil {
133+
return "", err
134+
}
135+
params := callToolRequestParams{
136+
Name: toolName,
137+
Arguments: payload,
138+
}
139+
140+
var result callToolResult
141+
if err := t.sendRequest(ctx, t.BaseURL(), "tools/call", params, headers, &result); err != nil {
142+
return "", fmt.Errorf("failed to invoke tool '%s': %w", toolName, err)
143+
}
144+
145+
if result.IsError {
146+
return "", fmt.Errorf("tool execution resulted in error")
147+
}
148+
149+
// Concatenate all text content blocks
150+
var sb strings.Builder
151+
for _, content := range result.Content {
152+
if content.Type == "text" {
153+
sb.WriteString(content.Text)
154+
}
155+
}
156+
157+
output := sb.String()
158+
if output == "" {
159+
// Return null if no text content found but not an error
160+
return "null", nil
161+
}
162+
return output, nil
163+
}
164+
165+
// initializeSession performs the initial handshake with the server.
166+
func (t *McpTransport) initializeSession(ctx context.Context) error {
167+
params := initializeRequestParams{
168+
ProtocolVersion: t.protocolVersion,
169+
Capabilities: clientCapabilities{},
170+
ClientInfo: implementation{
171+
Name: ClientName,
172+
Version: ClientVersion,
173+
},
174+
}
175+
176+
var result initializeResult
177+
if err := t.sendRequest(ctx, t.BaseURL(), "initialize", params, nil, &result); err != nil {
178+
return err
179+
}
180+
181+
// Protocol Version Check
182+
if result.ProtocolVersion != t.protocolVersion {
183+
return fmt.Errorf("MCP version mismatch: client (%s) != server (%s)", t.protocolVersion, result.ProtocolVersion)
184+
}
185+
186+
// Capabilities Check
187+
if result.Capabilities.Tools == nil {
188+
return fmt.Errorf("server does not support the 'tools' capability")
189+
}
190+
191+
t.ServerVersion = result.ServerInfo.Version
192+
193+
// Confirm Handshake
194+
return t.sendNotification(ctx, "notifications/initialized", map[string]any{})
195+
}
196+
197+
// sendRequest sends a standard JSON-RPC request to the server.
198+
func (t *McpTransport) sendRequest(ctx context.Context, url string, method string, params any, headers map[string]string, dest any) error {
199+
req := jsonRPCRequest{
200+
JSONRPC: "2.0",
201+
Method: method,
202+
ID: uuid.New().String(),
203+
Params: params,
204+
}
205+
return t.doRPC(ctx, url, req, headers, dest)
206+
}
207+
208+
// sendNotification sends a standard JSON-RPC notification (no response expected).
209+
func (t *McpTransport) sendNotification(ctx context.Context, method string, params any) error {
210+
req := jsonRPCNotification{
211+
JSONRPC: "2.0",
212+
Method: method,
213+
Params: params,
214+
}
215+
return t.doRPC(ctx, t.BaseURL(), req, nil, nil)
216+
}
217+
218+
// doRPC performs the low-level HTTP POST and handles JSON-RPC wrapping/unwrapping.
219+
// v2025-06-18: Injects 'MCP-Protocol-Version' header.
220+
func (t *McpTransport) doRPC(ctx context.Context, url string, reqBody any, headers map[string]string, dest any) error {
221+
payload, err := json.Marshal(reqBody)
222+
if err != nil {
223+
return fmt.Errorf("marshal failed: %w", err)
224+
}
225+
226+
// Create Request
227+
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(payload))
228+
if err != nil {
229+
return fmt.Errorf("create request failed: %w", err)
230+
}
231+
232+
httpReq.Header.Set("Content-Type", "application/json")
233+
// Set Accept header for MCP Spec 2025-03-26
234+
// Since SSE is not supported, we only accept application/json
235+
httpReq.Header.Set("Accept", "application/json")
236+
// v2025-06-18 Specific: Inject Protocol Version Header
237+
httpReq.Header.Set("MCP-Protocol-Version", t.protocolVersion)
238+
239+
// Apply resolved headers
240+
for k, v := range headers {
241+
httpReq.Header.Set(k, v)
242+
}
243+
244+
resp, err := t.HTTPClient.Do(httpReq)
245+
if err != nil {
246+
return fmt.Errorf("http request failed: %w", err)
247+
}
248+
defer resp.Body.Close()
249+
250+
if resp.StatusCode == http.StatusOK {
251+
// Continue to body parsing
252+
} else if (resp.StatusCode == http.StatusAccepted || resp.StatusCode == http.StatusNoContent) && dest == nil {
253+
return nil // Valid notification success
254+
} else {
255+
// Any other code, OR a 202/204 when we expected a result, is a failure.
256+
body, _ := io.ReadAll(resp.Body)
257+
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
258+
}
259+
260+
if dest == nil {
261+
return nil
262+
}
263+
264+
bodyBytes, err := io.ReadAll(resp.Body)
265+
if err != nil {
266+
return fmt.Errorf("read body failed: %w", err)
267+
}
268+
269+
// Decode RPC Envelope
270+
var rpcResp jsonRPCResponse
271+
if err := json.Unmarshal(bodyBytes, &rpcResp); err != nil {
272+
return fmt.Errorf("response unmarshal failed: %w", err)
273+
}
274+
275+
// Check RPC Error
276+
if rpcResp.Error != nil {
277+
return fmt.Errorf("MCP request failed with code %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
278+
}
279+
280+
// Decode Result into specific struct
281+
resultBytes, _ := json.Marshal(rpcResp.Result)
282+
if err := json.Unmarshal(resultBytes, dest); err != nil {
283+
return fmt.Errorf("failed to parse result data: %w", err)
284+
}
285+
286+
return nil
287+
}

0 commit comments

Comments
 (0)