chore: retire mark3labs/mcp-go dependency (#28061)

## Stack Context

PR 6 of 6 in a stack that migrates every Coder MCP surface from the
archived `github.com/mark3labs/mcp-go` library to the official
`github.com/modelcontextprotocol/go-sdk` v1.7.0.

Stack: #28056 -> #28057 -> #28058 -> #28059 -> #28060 -> #28061

## Why

With every production surface migrated, this PR removes the mark3labs
dependency entirely and converts the remaining test fixtures.

- Migrates the remaining mark3labs test fixtures (coderd MCP e2e tests,
chatd fixtures, mcpclient fixtures, and the Force On MCP policy tests)
to official stateless SDK servers.
- Removes `github.com/mark3labs/mcp-go` from `go.mod` and drops the
corresponding dependabot ignore entry. Zero references remain repo-wide.
- Updates the MCP docs for the 2026-07-28 protocol: stateless Streamable
HTTP behavior, the supported 2024-11-05 through 2026-07-28 protocol
range, and explicit non-features (resources, prompts, structured output,
elicitation, MCP Tasks).
- The e2e ping assertion is removed because MCP 2026-07-28 removed the
ping method.

> Mux created this PR on Mike's behalf.
This commit is contained in:
Michael Suchacz
2026-08-13 10:47:14 +00:00
committed by GitHub
parent c8e8b21a88
commit e92fd8e96f
13 changed files with 503 additions and 835 deletions
+177 -400
View File
@@ -16,9 +16,7 @@ import (
"testing"
"github.com/google/uuid"
mcpclient "github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/client/transport"
"github.com/mark3labs/mcp-go/mcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -58,43 +56,27 @@ func TestMCPHTTP_E2E_ClientIntegration(t *testing.T) {
// Create MCP client pointing to our endpoint
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// Configure client with authentication headers using RFC 6750 Bearer token
mcpClient := newIsolatedMCPClient(t, mcpURL,
transport.WithHTTPHeaders(map[string]string{
"Authorization": "Bearer " + coderClient.SessionToken(),
}))
mcpClient, err := newIsolatedMCPClient(ctx, mcpURL, "test-client", map[string]string{
"Authorization": "Bearer " + coderClient.SessionToken(),
})
require.NoError(t, err)
defer func() {
if closeErr := mcpClient.Close(); closeErr != nil {
t.Logf("Failed to close MCP client: %v", closeErr)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// Start client
err := mcpClient.Start(ctx)
require.NoError(t, err)
// Initialize connection
initReq := mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
ClientInfo: mcp.Implementation{
Name: "test-client",
Version: "1.0.0",
},
},
}
result, err := mcpClient.Initialize(ctx, initReq)
require.NoError(t, err)
result := mcpClient.InitializeResult()
require.Equal(t, mcpserver.MCPServerName, result.ServerInfo.Name)
require.Equal(t, mcp.LATEST_PROTOCOL_VERSION, result.ProtocolVersion)
require.Equal(t, "2026-07-28", result.ProtocolVersion)
require.NotNil(t, result.Capabilities)
// Test tool listing
tools, err := mcpClient.ListTools(ctx, mcp.ListToolsRequest{})
tools, err := mcpClient.ListTools(ctx, nil)
require.NoError(t, err)
require.NotEmpty(t, tools.Tools)
@@ -102,14 +84,13 @@ func TestMCPHTTP_E2E_ClientIntegration(t *testing.T) {
var foundTools []string
var userTool *mcp.Tool
var writeFileTool *mcp.Tool
for i := range tools.Tools {
tool := tools.Tools[i]
for _, tool := range tools.Tools {
foundTools = append(foundTools, tool.Name)
switch tool.Name {
case toolsdk.ToolNameGetAuthenticatedUser:
userTool = &tools.Tools[i]
userTool = tool
case toolsdk.ToolNameWorkspaceWriteFile:
writeFileTool = &tools.Tools[i]
writeFileTool = tool
}
}
@@ -117,50 +98,39 @@ func TestMCPHTTP_E2E_ClientIntegration(t *testing.T) {
assert.Contains(t, foundTools, toolsdk.ToolNameGetAuthenticatedUser, "Should have authenticated user tool")
require.NotNil(t, userTool)
require.NotNil(t, writeFileTool)
require.NotNil(t, userTool.Annotations.ReadOnlyHint)
require.NotNil(t, userTool.Annotations)
require.NotNil(t, userTool.Annotations.DestructiveHint)
require.NotNil(t, userTool.Annotations.IdempotentHint)
require.NotNil(t, userTool.Annotations.OpenWorldHint)
assert.True(t, *userTool.Annotations.ReadOnlyHint)
assert.True(t, userTool.Annotations.ReadOnlyHint)
assert.False(t, *userTool.Annotations.DestructiveHint)
assert.True(t, *userTool.Annotations.IdempotentHint)
assert.True(t, userTool.Annotations.IdempotentHint)
assert.False(t, *userTool.Annotations.OpenWorldHint)
require.NotNil(t, writeFileTool.Annotations.ReadOnlyHint)
require.NotNil(t, writeFileTool.Annotations)
require.NotNil(t, writeFileTool.Annotations.DestructiveHint)
require.NotNil(t, writeFileTool.Annotations.IdempotentHint)
require.NotNil(t, writeFileTool.Annotations.OpenWorldHint)
assert.False(t, *writeFileTool.Annotations.ReadOnlyHint)
assert.False(t, writeFileTool.Annotations.ReadOnlyHint)
assert.True(t, *writeFileTool.Annotations.DestructiveHint)
assert.False(t, *writeFileTool.Annotations.IdempotentHint)
assert.False(t, writeFileTool.Annotations.IdempotentHint)
assert.False(t, *writeFileTool.Annotations.OpenWorldHint)
// Execute the authenticated user tool.
require.NotNil(t, userTool, "Expected to find "+toolsdk.ToolNameGetAuthenticatedUser+" tool")
// Execute the tool
toolReq := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: userTool.Name,
Arguments: map[string]any{},
},
}
toolResult, err := mcpClient.CallTool(ctx, toolReq)
toolResult, err := mcpClient.CallTool(ctx, &mcp.CallToolParams{
Name: userTool.Name,
Arguments: map[string]any{},
})
require.NoError(t, err)
require.NotEmpty(t, toolResult.Content)
// Verify the result contains user information
assert.Len(t, toolResult.Content, 1)
if textContent, ok := toolResult.Content[0].(mcp.TextContent); ok {
assert.Equal(t, "text", textContent.Type)
if textContent, ok := toolResult.Content[0].(*mcp.TextContent); ok {
assert.NotEmpty(t, textContent.Text)
} else {
t.Errorf("Expected TextContent type, got %T", toolResult.Content[0])
}
// Test ping functionality
err = mcpClient.Ping(ctx)
require.NoError(t, err)
}
func TestMCPHTTP_E2E_UnauthenticatedAccess(t *testing.T) {
@@ -191,32 +161,7 @@ func TestMCPHTTP_E2E_UnauthenticatedAccess(t *testing.T) {
require.Equal(t, http.StatusUnauthorized, resp.StatusCode, "Should get HTTP 401 for unauthenticated access")
// Also test with MCP client to ensure it handles the error gracefully
mcpClient := newIsolatedMCPClient(t, mcpURL)
defer func() {
if closeErr := mcpClient.Close(); closeErr != nil {
t.Logf("Failed to close MCP client: %v", closeErr)
}
}()
// Start client and try to initialize - this should fail due to authentication
err = mcpClient.Start(ctx)
if err != nil {
// Authentication failed at transport level - this is expected
t.Logf("Unauthenticated access test successful: Transport-level authentication error: %v", err)
return
}
initReq := mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
ClientInfo: mcp.Implementation{
Name: "test-client-unauth",
Version: "1.0.0",
},
},
}
_, err = mcpClient.Initialize(ctx, initReq)
_, err = newIsolatedMCPClient(ctx, mcpURL, "test-client-unauth", nil)
require.Error(t, err, "Should fail during MCP initialization without authentication")
}
@@ -245,44 +190,30 @@ func TestMCPHTTP_E2E_ToolWithWorkspace(t *testing.T) {
coderdtest.NewWorkspaceAgentWaiter(t, coderClient, r.Workspace.ID).Wait()
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
mcpClient := newIsolatedMCPClient(t, mcpURL,
transport.WithHTTPHeaders(map[string]string{
"Authorization": "Bearer " + coderClient.SessionToken(),
}))
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
mcpClient, err := newIsolatedMCPClient(ctx, mcpURL, "test-client-workspace", map[string]string{
"Authorization": "Bearer " + coderClient.SessionToken(),
})
require.NoError(t, err)
defer func() {
if closeErr := mcpClient.Close(); closeErr != nil {
t.Logf("Failed to close MCP client: %v", closeErr)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
require.NoError(t, mcpClient.Start(ctx))
_, err := mcpClient.Initialize(ctx, mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
ClientInfo: mcp.Implementation{
Name: "test-client-workspace",
Version: "1.0.0",
},
},
})
require.NoError(t, err)
toolResult, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: toolsdk.ToolNameWorkspaceLS,
Arguments: map[string]any{
"workspace": r.Workspace.Name,
"path": tmpdir,
},
toolResult, err := mcpClient.CallTool(ctx, &mcp.CallToolParams{
Name: toolsdk.ToolNameWorkspaceLS,
Arguments: map[string]any{
"workspace": r.Workspace.Name,
"path": tmpdir,
},
})
require.NoError(t, err)
require.NotEmpty(t, toolResult.Content)
textContent, ok := toolResult.Content[0].(mcp.TextContent)
textContent, ok := toolResult.Content[0].(*mcp.TextContent)
require.True(t, ok, "expected TextContent type, got %T", toolResult.Content[0])
var response toolsdk.WorkspaceLSResponse
@@ -306,45 +237,24 @@ func TestMCPHTTP_E2E_ErrorHandling(t *testing.T) {
// Create MCP client
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
mcpClient := newIsolatedMCPClient(t, mcpURL,
transport.WithHTTPHeaders(map[string]string{
"Authorization": "Bearer " + coderClient.SessionToken(),
}))
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
mcpClient, err := newIsolatedMCPClient(ctx, mcpURL, "test-client-errors", map[string]string{
"Authorization": "Bearer " + coderClient.SessionToken(),
})
require.NoError(t, err)
defer func() {
if closeErr := mcpClient.Close(); closeErr != nil {
t.Logf("Failed to close MCP client: %v", closeErr)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// Start and initialize client
err := mcpClient.Start(ctx)
require.NoError(t, err)
initReq := mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
ClientInfo: mcp.Implementation{
Name: "test-client-errors",
Version: "1.0.0",
},
},
}
_, err = mcpClient.Initialize(ctx, initReq)
require.NoError(t, err)
// Test calling non-existent tool
toolReq := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: "nonexistent_tool",
Arguments: map[string]any{},
},
}
_, err = mcpClient.CallTool(ctx, toolReq)
_, err = mcpClient.CallTool(ctx, &mcp.CallToolParams{
Name: "nonexistent_tool",
Arguments: map[string]any{},
})
require.Error(t, err, "Should get error when calling non-existent tool")
require.Contains(t, err.Error(), "nonexistent_tool", "Should mention the tool name in error message")
@@ -364,36 +274,19 @@ func TestMCPHTTP_E2E_ConcurrentRequests(t *testing.T) {
// Create MCP client
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
mcpClient := newIsolatedMCPClient(t, mcpURL,
transport.WithHTTPHeaders(map[string]string{
"Authorization": "Bearer " + coderClient.SessionToken(),
}))
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
mcpClient, err := newIsolatedMCPClient(ctx, mcpURL, "test-client-concurrent", map[string]string{
"Authorization": "Bearer " + coderClient.SessionToken(),
})
require.NoError(t, err)
defer func() {
if closeErr := mcpClient.Close(); closeErr != nil {
t.Logf("Failed to close MCP client: %v", closeErr)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// Start and initialize client
err := mcpClient.Start(ctx)
require.NoError(t, err)
initReq := mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
ClientInfo: mcp.Implementation{
Name: "test-client-concurrent",
Version: "1.0.0",
},
},
}
_, err = mcpClient.Initialize(ctx, initReq)
require.NoError(t, err)
// Test concurrent tool listings
const numConcurrent = 5
eg, egCtx := errgroup.WithContext(ctx)
@@ -403,7 +296,7 @@ func TestMCPHTTP_E2E_ConcurrentRequests(t *testing.T) {
reqCtx, reqCancel := context.WithTimeout(egCtx, testutil.WaitLong)
defer reqCancel()
tools, err := mcpClient.ListTools(reqCtx, mcp.ListToolsRequest{})
tools, err := mcpClient.ListTools(reqCtx, nil)
if err != nil {
return err
}
@@ -518,39 +411,23 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
sessionToken := coderClient.SessionToken()
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
mcpClient := newIsolatedMCPClient(t, mcpURL,
transport.WithHTTPHeaders(map[string]string{
"Authorization": "Bearer " + sessionToken,
}))
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
mcpClient, err := newIsolatedMCPClient(ctx, mcpURL, "test-oauth2-client", map[string]string{
"Authorization": "Bearer " + sessionToken,
})
require.NoError(t, err)
defer func() {
if closeErr := mcpClient.Close(); closeErr != nil {
t.Logf("Failed to close MCP client: %v", closeErr)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// Start and initialize MCP client with Bearer token
err = mcpClient.Start(ctx)
require.NoError(t, err)
initReq := mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
ClientInfo: mcp.Implementation{
Name: "test-oauth2-client",
Version: "1.0.0",
},
},
}
result, err := mcpClient.Initialize(ctx, initReq)
require.NoError(t, err)
require.Equal(t, mcpserver.MCPServerName, result.ServerInfo.Name)
require.Equal(t, mcpserver.MCPServerName, mcpClient.InitializeResult().ServerInfo.Name)
// Test tool listing with OAuth2 Bearer token
tools, err := mcpClient.ListTools(ctx, mcp.ListToolsRequest{})
tools, err := mcpClient.ListTools(ctx, nil)
require.NoError(t, err)
require.NotEmpty(t, tools.Tools)
@@ -666,36 +543,20 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
// Step 3: Use access token to authenticate with MCP endpoint
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
mcpClient := newIsolatedMCPClient(t, mcpURL,
transport.WithHTTPHeaders(map[string]string{
"Authorization": "Bearer " + accessToken,
}))
mcpClient, err := newIsolatedMCPClient(ctx, mcpURL, "test-oauth2-flow-client", map[string]string{
"Authorization": "Bearer " + accessToken,
})
require.NoError(t, err)
defer func() {
if closeErr := mcpClient.Close(); closeErr != nil {
t.Logf("Failed to close MCP client: %v", closeErr)
}
}()
// Initialize and test the MCP connection with OAuth2 access token
err = mcpClient.Start(ctx)
require.NoError(t, err)
initReq := mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
ClientInfo: mcp.Implementation{
Name: "test-oauth2-flow-client",
Version: "1.0.0",
},
},
}
result, err := mcpClient.Initialize(ctx, initReq)
require.NoError(t, err)
require.Equal(t, mcpserver.MCPServerName, result.ServerInfo.Name)
require.Equal(t, mcpserver.MCPServerName, mcpClient.InitializeResult().ServerInfo.Name)
// Test tool execution with OAuth2 access token
tools, err := mcpClient.ListTools(ctx, mcp.ListToolsRequest{})
tools, err := mcpClient.ListTools(ctx, nil)
require.NoError(t, err)
require.NotEmpty(t, tools.Tools)
@@ -703,17 +564,15 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
var userTool *mcp.Tool
for _, tool := range tools.Tools {
if tool.Name == toolsdk.ToolNameGetAuthenticatedUser {
userTool = &tool
userTool = tool
break
}
}
require.NotNil(t, userTool, "Expected to find "+toolsdk.ToolNameGetAuthenticatedUser+" tool")
toolReq := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: userTool.Name,
Arguments: map[string]any{},
},
toolReq := &mcp.CallToolParams{
Name: userTool.Name,
Arguments: map[string]any{},
}
toolResult, err := mcpClient.CallTool(ctx, toolReq)
@@ -758,36 +617,20 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
t.Logf("Successfully refreshed token: %s...", newAccessToken[:10])
// Step 5: Use new access token to create another MCP connection
newMcpClient := newIsolatedMCPClient(t, mcpURL,
transport.WithHTTPHeaders(map[string]string{
"Authorization": "Bearer " + newAccessToken,
}))
newMcpClient, err := newIsolatedMCPClient(ctx, mcpURL, "test-refreshed-token-client", map[string]string{
"Authorization": "Bearer " + newAccessToken,
})
require.NoError(t, err)
defer func() {
if closeErr := newMcpClient.Close(); closeErr != nil {
t.Logf("Failed to close new MCP client: %v", closeErr)
t.Logf("Failed to close MCP client: %v", closeErr)
}
}()
// Test the new token works
err = newMcpClient.Start(ctx)
require.NoError(t, err)
newInitReq := mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
ClientInfo: mcp.Implementation{
Name: "test-refreshed-token-client",
Version: "1.0.0",
},
},
}
newResult, err := newMcpClient.Initialize(ctx, newInitReq)
require.NoError(t, err)
require.Equal(t, mcpserver.MCPServerName, newResult.ServerInfo.Name)
require.Equal(t, mcpserver.MCPServerName, newMcpClient.InitializeResult().ServerInfo.Name)
// Verify we can still execute tools with the refreshed token
newTools, err := newMcpClient.ListTools(ctx, mcp.ListToolsRequest{})
newTools, err := newMcpClient.ListTools(ctx, nil)
require.NoError(t, err)
require.NotEmpty(t, newTools.Tools)
@@ -985,36 +828,20 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
t.Logf("Successfully obtained access token: %s...", accessToken[:10])
// Step 5: Use access token to get user information via MCP
mcpClient := newIsolatedMCPClient(t, mcpURL,
transport.WithHTTPHeaders(map[string]string{
"Authorization": "Bearer " + accessToken,
}))
mcpClient, err := newIsolatedMCPClient(ctx, mcpURL, "test-dynamic-client", map[string]string{
"Authorization": "Bearer " + accessToken,
})
require.NoError(t, err)
defer func() {
if closeErr := mcpClient.Close(); closeErr != nil {
t.Logf("Failed to close MCP client: %v", closeErr)
}
}()
// Initialize MCP connection
err = mcpClient.Start(ctx)
require.NoError(t, err)
initReq := mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
ClientInfo: mcp.Implementation{
Name: "test-dynamic-client",
Version: "1.0.0",
},
},
}
result, err := mcpClient.Initialize(ctx, initReq)
require.NoError(t, err)
require.Equal(t, mcpserver.MCPServerName, result.ServerInfo.Name)
require.Equal(t, mcpserver.MCPServerName, mcpClient.InitializeResult().ServerInfo.Name)
// Get user information
tools, err := mcpClient.ListTools(ctx, mcp.ListToolsRequest{})
tools, err := mcpClient.ListTools(ctx, nil)
require.NoError(t, err)
require.NotEmpty(t, tools.Tools)
@@ -1022,17 +849,15 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
var userTool *mcp.Tool
for _, tool := range tools.Tools {
if tool.Name == toolsdk.ToolNameGetAuthenticatedUser {
userTool = &tool
userTool = tool
break
}
}
require.NotNil(t, userTool, "Expected to find "+toolsdk.ToolNameGetAuthenticatedUser+" tool")
toolReq := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: userTool.Name,
Arguments: map[string]any{},
},
toolReq := &mcp.CallToolParams{
Name: userTool.Name,
Arguments: map[string]any{},
}
toolResult, err := mcpClient.CallTool(ctx, toolReq)
@@ -1041,7 +866,7 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
// Extract user info from first token
var firstUserInfo string
if textContent, ok := toolResult.Content[0].(mcp.TextContent); ok {
if textContent, ok := toolResult.Content[0].(*mcp.TextContent); ok {
firstUserInfo = textContent.Text
} else {
t.Errorf("Expected TextContent type, got %T", toolResult.Content[0])
@@ -1082,36 +907,20 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
t.Logf("Successfully refreshed token: %s...", newAccessToken[:10])
// Step 7: Use refreshed token to get user information again via MCP
newMcpClient := newIsolatedMCPClient(t, mcpURL,
transport.WithHTTPHeaders(map[string]string{
"Authorization": "Bearer " + newAccessToken,
}))
newMcpClient, err := newIsolatedMCPClient(ctx, mcpURL, "test-dynamic-client-refreshed", map[string]string{
"Authorization": "Bearer " + newAccessToken,
})
require.NoError(t, err)
defer func() {
if closeErr := newMcpClient.Close(); closeErr != nil {
t.Logf("Failed to close new MCP client: %v", closeErr)
t.Logf("Failed to close MCP client: %v", closeErr)
}
}()
// Initialize new MCP connection
err = newMcpClient.Start(ctx)
require.NoError(t, err)
newInitReq := mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
ClientInfo: mcp.Implementation{
Name: "test-dynamic-client-refreshed",
Version: "1.0.0",
},
},
}
newResult, err := newMcpClient.Initialize(ctx, newInitReq)
require.NoError(t, err)
require.Equal(t, mcpserver.MCPServerName, newResult.ServerInfo.Name)
require.Equal(t, mcpserver.MCPServerName, newMcpClient.InitializeResult().ServerInfo.Name)
// Get user information with refreshed token
newTools, err := newMcpClient.ListTools(ctx, mcp.ListToolsRequest{})
newTools, err := newMcpClient.ListTools(ctx, nil)
require.NoError(t, err)
require.NotEmpty(t, newTools.Tools)
@@ -1122,7 +931,7 @@ func TestMCPHTTP_E2E_OAuth2_EndToEnd(t *testing.T) {
// Extract user info from refreshed token
var secondUserInfo string
if textContent, ok := newToolResult.Content[0].(mcp.TextContent); ok {
if textContent, ok := newToolResult.Content[0].(*mcp.TextContent); ok {
secondUserInfo = textContent.Text
} else {
t.Errorf("Expected TextContent type, got %T", newToolResult.Content[0])
@@ -1260,43 +1069,27 @@ func TestMCPHTTP_E2E_ChatGPTEndpoint(t *testing.T) {
// Create MCP client pointing to the ChatGPT endpoint
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint + "?toolset=chatgpt"
ctx, cancel := context.WithTimeout(t.Context(), testutil.WaitLong)
defer cancel()
// Configure client with authentication headers using RFC 6750 Bearer token
mcpClient := newIsolatedMCPClient(t, mcpURL,
transport.WithHTTPHeaders(map[string]string{
"Authorization": "Bearer " + coderClient.SessionToken(),
}))
mcpClient, err := newIsolatedMCPClient(ctx, mcpURL, "test-chatgpt-client", map[string]string{
"Authorization": "Bearer " + coderClient.SessionToken(),
})
require.NoError(t, err)
t.Cleanup(func() {
if closeErr := mcpClient.Close(); closeErr != nil {
t.Logf("Failed to close MCP client: %v", closeErr)
}
})
ctx, cancel := context.WithTimeout(t.Context(), testutil.WaitLong)
defer cancel()
// Start client
err := mcpClient.Start(ctx)
require.NoError(t, err)
// Initialize connection
initReq := mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
ClientInfo: mcp.Implementation{
Name: "test-chatgpt-client",
Version: "1.0.0",
},
},
}
result, err := mcpClient.Initialize(ctx, initReq)
require.NoError(t, err)
result := mcpClient.InitializeResult()
require.Equal(t, mcpserver.MCPServerName, result.ServerInfo.Name)
require.Equal(t, mcp.LATEST_PROTOCOL_VERSION, result.ProtocolVersion)
require.Equal(t, "2026-07-28", result.ProtocolVersion)
require.NotNil(t, result.Capabilities)
// Test tool listing - should only have search and fetch tools for ChatGPT
tools, err := mcpClient.ListTools(ctx, mcp.ListToolsRequest{})
tools, err := mcpClient.ListTools(ctx, nil)
require.NoError(t, err)
require.NotEmpty(t, tools.Tools)
@@ -1321,19 +1114,17 @@ func TestMCPHTTP_E2E_ChatGPTEndpoint(t *testing.T) {
var searchTool *mcp.Tool
for _, tool := range tools.Tools {
if tool.Name == toolsdk.ToolNameChatGPTSearch {
searchTool = &tool
searchTool = tool
break
}
}
require.NotNil(t, searchTool, "Expected to find search tool")
// Execute search for templates
searchReq := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: searchTool.Name,
Arguments: map[string]any{
"query": "templates",
},
searchReq := &mcp.CallToolParams{
Name: searchTool.Name,
Arguments: map[string]any{
"query": "templates",
},
}
@@ -1343,8 +1134,7 @@ func TestMCPHTTP_E2E_ChatGPTEndpoint(t *testing.T) {
// Verify the search result contains our template
assert.Len(t, searchResult.Content, 1)
if textContent, ok := searchResult.Content[0].(mcp.TextContent); ok {
assert.Equal(t, "text", textContent.Type)
if textContent, ok := searchResult.Content[0].(*mcp.TextContent); ok {
assert.Contains(t, textContent.Text, template.ID.String(), "Search result should contain our test template")
t.Logf("Search result: %s", textContent.Text)
} else {
@@ -1355,19 +1145,17 @@ func TestMCPHTTP_E2E_ChatGPTEndpoint(t *testing.T) {
var fetchTool *mcp.Tool
for _, tool := range tools.Tools {
if tool.Name == toolsdk.ToolNameChatGPTFetch {
fetchTool = &tool
fetchTool = tool
break
}
}
require.NotNil(t, fetchTool, "Expected to find fetch tool")
// Execute fetch for the template
fetchReq := mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: fetchTool.Name,
Arguments: map[string]any{
"id": fmt.Sprintf("template:%s", template.ID.String()),
},
fetchReq := &mcp.CallToolParams{
Name: fetchTool.Name,
Arguments: map[string]any{
"id": fmt.Sprintf("template:%s", template.ID.String()),
},
}
@@ -1377,8 +1165,7 @@ func TestMCPHTTP_E2E_ChatGPTEndpoint(t *testing.T) {
// Verify the fetch result contains template details
assert.Len(t, fetchResult.Content, 1)
if textContent, ok := fetchResult.Content[0].(mcp.TextContent); ok {
assert.Equal(t, "text", textContent.Type)
if textContent, ok := fetchResult.Content[0].(*mcp.TextContent); ok {
assert.Contains(t, textContent.Text, template.Name, "Fetch result should contain template name")
assert.Contains(t, textContent.Text, template.ID.String(), "Fetch result should contain template ID")
t.Logf("Fetch result contains template data")
@@ -1425,41 +1212,27 @@ func TestMCPHTTP_E2E_WorkspaceSSHAuthz(t *testing.T) {
// Connect with the template-admin user.
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
mcpClient := newIsolatedMCPClient(t, mcpURL,
transport.WithHTTPHeaders(map[string]string{
"Authorization": "Bearer " + tmplAdminClient.SessionToken(),
}))
defer func() {
_ = mcpClient.Close()
}()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
require.NoError(t, mcpClient.Start(ctx))
_, err := mcpClient.Initialize(ctx, mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
ClientInfo: mcp.Implementation{
Name: "test-client-authz",
Version: "1.0.0",
},
},
mcpClient, err := newIsolatedMCPClient(ctx, mcpURL, "test-client-authz", map[string]string{
"Authorization": "Bearer " + tmplAdminClient.SessionToken(),
})
require.NoError(t, err)
defer func() {
_ = mcpClient.Close()
}()
// Calling a workspace tool that requires an agent connection
// should fail because the template-admin user lacks ActionSSH.
// Use owner/workspace format so the lookup resolves to the
// admin's workspace rather than defaulting to "me".
workspaceIdent := coderdtest.FirstUserParams.Username + "/" + r.Workspace.Name
toolResult, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: toolsdk.ToolNameWorkspaceReadFile,
Arguments: map[string]any{
"workspace": workspaceIdent,
"path": "/tmp/secret.txt",
},
toolResult, err := mcpClient.CallTool(ctx, &mcp.CallToolParams{
Name: toolsdk.ToolNameWorkspaceReadFile,
Arguments: map[string]any{
"workspace": workspaceIdent,
"path": "/tmp/secret.txt",
},
})
// The MCP library may return the error in the tool result itself
@@ -1470,7 +1243,7 @@ func TestMCPHTTP_E2E_WorkspaceSSHAuthz(t *testing.T) {
}
// If no Go error, the tool result must report failure.
require.True(t, toolResult.IsError, "expected tool call to fail for user without SSH access")
textContent, ok := toolResult.Content[0].(mcp.TextContent)
textContent, ok := toolResult.Content[0].(*mcp.TextContent)
require.True(t, ok)
assert.Contains(t, textContent.Text, "unauthorized")
}
@@ -1481,18 +1254,35 @@ func mustParseURL(t *testing.T, rawURL string) *url.URL {
return u
}
// newIsolatedMCPClient creates a streamable HTTP MCP client that uses
// an isolated http.Transport cloned from http.DefaultTransport.
// This prevents httptest.Server.Close() (which calls
// http.DefaultTransport.CloseIdleConnections()) from disrupting the
// client's connections during parallel tests.
func newIsolatedMCPClient(t *testing.T, mcpURL string, opts ...transport.StreamableHTTPCOption) *mcpclient.Client {
t.Helper()
// newIsolatedMCPClient connects through a transport isolated from
// http.DefaultTransport, preventing parallel httptest cleanup from closing
// the client's idle connections.
func newIsolatedMCPClient(ctx context.Context, mcpURL, name string, headers map[string]string) (*mcp.ClientSession, error) {
isolated := coderdtest.NewIsolatedHTTPClient(nil)
opts = append([]transport.StreamableHTTPCOption{transport.WithHTTPBasicClient(isolated)}, opts...)
client, err := mcpclient.NewStreamableHttpClient(mcpURL, opts...)
require.NoError(t, err)
return client
if len(headers) > 0 {
isolated.Transport = &headerRoundTripper{
base: isolated.Transport,
headers: headers,
}
}
client := mcp.NewClient(&mcp.Implementation{Name: name, Version: "1.0.0"}, nil)
return client.Connect(ctx, &mcp.StreamableClientTransport{
Endpoint: mcpURL,
HTTPClient: isolated,
}, nil)
}
type headerRoundTripper struct {
base http.RoundTripper
headers map[string]string
}
func (h *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
clone := req.Clone(req.Context())
for key, value := range h.headers {
clone.Header.Set(key, value)
}
return h.base.RoundTrip(clone)
}
// sentinelTransport wraps an http.RoundTripper and counts how many
@@ -1508,12 +1298,6 @@ func (s *sentinelTransport) RoundTrip(req *http.Request) (*http.Response, error)
return s.inner.RoundTrip(req)
}
// TestMCPHTTP_E2E_TransportIsolation verifies that the
// newIsolatedMCPClient helper creates clients that do NOT route
// requests through http.DefaultTransport, while raw
// mcpclient.NewStreamableHttpClient (without explicit
// WithHTTPBasicClient) does use it.
//
//nolint:paralleltest // Mutates http.DefaultTransport.
func TestMCPHTTP_E2E_TransportIsolation(t *testing.T) {
// Replace DefaultTransport with a counting sentinel.
@@ -1527,29 +1311,25 @@ func TestMCPHTTP_E2E_TransportIsolation(t *testing.T) {
_ = coderdtest.CreateFirstUser(t, coderClient)
mcpURL := api.AccessURL.String() + mcpserver.MCPEndpoint
authOpt := transport.WithHTTPHeaders(map[string]string{
authHeaders := map[string]string{
"Authorization": "Bearer " + coderClient.SessionToken(),
})
}
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
initReq := mcp.InitializeRequest{
Params: mcp.InitializeParams{
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
ClientInfo: mcp.Implementation{Name: "sentinel-test", Version: "1.0.0"},
},
}
t.Run("RawClientUsesDefaultTransport", func(t *testing.T) {
sentinel.hits.Store(0)
rawClient, err := mcpclient.NewStreamableHttpClient(mcpURL, authOpt)
require.NoError(t, err)
defer func() { _ = rawClient.Close() }()
require.NoError(t, rawClient.Start(ctx))
_, err = rawClient.Initialize(ctx, initReq)
rawClient := mcp.NewClient(&mcp.Implementation{Name: "sentinel-test", Version: "1.0.0"}, nil)
rawSession, err := rawClient.Connect(ctx, &mcp.StreamableClientTransport{
Endpoint: mcpURL,
HTTPClient: &http.Client{Transport: &headerRoundTripper{
base: http.DefaultTransport,
headers: authHeaders,
}},
}, nil)
require.NoError(t, err)
defer func() { _ = rawSession.Close() }()
require.Greater(t, sentinel.hits.Load(), int64(0),
"raw client should route requests through http.DefaultTransport")
@@ -1557,12 +1337,9 @@ func TestMCPHTTP_E2E_TransportIsolation(t *testing.T) {
t.Run("IsolatedClientBypassesDefaultTransport", func(t *testing.T) {
sentinel.hits.Store(0)
isoClient := newIsolatedMCPClient(t, mcpURL, authOpt)
defer func() { _ = isoClient.Close() }()
require.NoError(t, isoClient.Start(ctx))
_, err := isoClient.Initialize(ctx, initReq)
isoClient, err := newIsolatedMCPClient(ctx, mcpURL, "sentinel-test", authHeaders)
require.NoError(t, err)
defer func() { _ = isoClient.Close() }()
require.Equal(t, int64(0), sentinel.hits.Load(),
"isolated client must NOT route requests through http.DefaultTransport")
+2 -3
View File
@@ -8,7 +8,6 @@ import (
"strings"
"testing"
"github.com/mark3labs/mcp-go/mcp"
sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -59,7 +58,7 @@ func TestMCPHTTP_InitializeRequest(t *testing.T) {
"id": 1,
"method": "initialize",
"params": map[string]any{
"protocolVersion": mcp.LATEST_PROTOCOL_VERSION,
"protocolVersion": "2025-06-18",
"capabilities": map[string]any{},
"clientInfo": map[string]any{
"name": "test-client",
@@ -97,7 +96,7 @@ func TestMCPHTTP_InitializeRequest(t *testing.T) {
result, ok := response["result"].(map[string]any)
require.True(t, ok)
assert.Equal(t, mcp.LATEST_PROTOCOL_VERSION, result["protocolVersion"])
assert.Equal(t, "2025-06-18", result["protocolVersion"])
assert.Contains(t, result, "capabilities")
assert.Contains(t, result, "serverInfo")
}