mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
## 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.
216 lines
6.1 KiB
Go
216 lines
6.1 KiB
Go
package mcp_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
mcpserver "github.com/coder/coder/v2/coderd/mcp"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/codersdk/toolsdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestMCPServer_Creation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := testutil.Logger(t)
|
|
|
|
server, err := mcpserver.NewServer(logger)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, server)
|
|
}
|
|
|
|
func TestMCPServer_Handler(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := testutil.Logger(t)
|
|
|
|
server, err := mcpserver.NewServer(logger)
|
|
require.NoError(t, err)
|
|
|
|
// Test that server implements http.Handler interface
|
|
var handler http.Handler = server
|
|
require.NotNil(t, handler)
|
|
}
|
|
|
|
func TestMCPHTTP_InitializeRequest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := testutil.Logger(t)
|
|
|
|
server, err := mcpserver.NewServer(logger)
|
|
require.NoError(t, err)
|
|
|
|
// Use server directly as http.Handler
|
|
handler := server
|
|
|
|
// Create initialize request
|
|
initRequest := map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "initialize",
|
|
"params": map[string]any{
|
|
"protocolVersion": "2025-06-18",
|
|
"capabilities": map[string]any{},
|
|
"clientInfo": map[string]any{
|
|
"name": "test-client",
|
|
"version": "1.0.0",
|
|
},
|
|
},
|
|
}
|
|
|
|
body, err := json.Marshal(initRequest)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json,text/event-stream")
|
|
|
|
recorder := httptest.NewRecorder()
|
|
handler.ServeHTTP(recorder, req)
|
|
|
|
if recorder.Code != http.StatusOK {
|
|
t.Logf("Response body: %s", recorder.Body.String())
|
|
}
|
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
|
|
|
sessionID := recorder.Header().Get("Mcp-Session-Id")
|
|
assert.Empty(t, sessionID)
|
|
|
|
// Parse response
|
|
var response map[string]any
|
|
err = json.Unmarshal(recorder.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "2.0", response["jsonrpc"])
|
|
assert.Equal(t, float64(1), response["id"])
|
|
|
|
result, ok := response["result"].(map[string]any)
|
|
require.True(t, ok)
|
|
|
|
assert.Equal(t, "2025-06-18", result["protocolVersion"])
|
|
assert.Contains(t, result, "capabilities")
|
|
assert.Contains(t, result, "serverInfo")
|
|
}
|
|
|
|
func TestMCPHTTP_ToolRegistration(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := testutil.Logger(t)
|
|
|
|
server, err := mcpserver.NewServer(logger)
|
|
require.NoError(t, err)
|
|
|
|
// Test registering tools with nil client should return error
|
|
err = server.RegisterTools(nil)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "client cannot be nil", "Should reject nil client with appropriate error message")
|
|
|
|
// Test registering tools with valid client should succeed
|
|
client := codersdk.New(testutil.MustURL(t, "http://not-used"))
|
|
err = server.RegisterTools(client)
|
|
require.NoError(t, err)
|
|
|
|
// Verify that all expected tools are available in the toolsdk
|
|
expectedToolCount := len(toolsdk.All)
|
|
require.Greater(t, expectedToolCount, 0, "Should have some tools available")
|
|
|
|
// Verify specific tools are present by checking tool names
|
|
toolNames := make([]string, len(toolsdk.All))
|
|
for i, tool := range toolsdk.All {
|
|
toolNames[i] = tool.Name
|
|
}
|
|
require.Contains(t, toolNames, toolsdk.ToolNameReportTask, "Should include ReportTask (UserClientOptional)")
|
|
require.Contains(t, toolNames, toolsdk.ToolNameGetAuthenticatedUser, "Should include GetAuthenticatedUser (requires auth)")
|
|
}
|
|
|
|
func TestMCPHTTP_ModernProtocol(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := testutil.Logger(t)
|
|
|
|
server, err := mcpserver.NewServer(logger)
|
|
require.NoError(t, err)
|
|
client := codersdk.New(testutil.MustURL(t, "http://not-used"))
|
|
err = server.RegisterTools(client)
|
|
require.NoError(t, err)
|
|
|
|
ts := httptest.NewServer(server)
|
|
defer ts.Close()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
mcpClient := sdkmcp.NewClient(&sdkmcp.Implementation{Name: "test-client", Version: "1.0.0"}, nil)
|
|
session, err := mcpClient.Connect(ctx, &sdkmcp.StreamableClientTransport{
|
|
Endpoint: ts.URL,
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
defer session.Close()
|
|
|
|
init := session.InitializeResult()
|
|
require.Equal(t, "2026-07-28", init.ProtocolVersion)
|
|
require.Equal(t, mcpserver.MCPServerName, init.ServerInfo.Name)
|
|
require.Equal(t, mcpserver.MCPServerInstructions, init.Instructions)
|
|
|
|
tools, err := session.ListTools(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, tools.Tools)
|
|
}
|
|
|
|
func TestMCPHTTP_UnsupportedProtocolVersion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := testutil.Logger(t)
|
|
|
|
server, err := mcpserver.NewServer(logger)
|
|
require.NoError(t, err)
|
|
|
|
body := `{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{` +
|
|
`"io.modelcontextprotocol/protocolVersion":"2099-01-01",` +
|
|
`"io.modelcontextprotocol/clientInfo":{"name":"test","version":"1.0"},` +
|
|
`"io.modelcontextprotocol/clientCapabilities":{}}}}`
|
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json,text/event-stream")
|
|
req.Header.Set("MCP-Protocol-Version", "2099-01-01")
|
|
req.Header.Set("Mcp-Method", "tools/list")
|
|
|
|
recorder := httptest.NewRecorder()
|
|
server.ServeHTTP(recorder, req)
|
|
|
|
var response struct {
|
|
Error struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response), "body: %s", recorder.Body.String())
|
|
require.Equal(t, -32022, response.Error.Code)
|
|
}
|
|
|
|
func TestMCPHTTP_TransportMethods(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := testutil.Logger(t)
|
|
|
|
server, err := mcpserver.NewServer(logger)
|
|
require.NoError(t, err)
|
|
|
|
for _, method := range []string{http.MethodGet, http.MethodDelete} {
|
|
req := httptest.NewRequest(method, "/", nil)
|
|
if method == http.MethodGet {
|
|
req.Header.Set("Accept", "text/event-stream")
|
|
}
|
|
recorder := httptest.NewRecorder()
|
|
server.ServeHTTP(recorder, req)
|
|
require.Equal(t, http.StatusMethodNotAllowed, recorder.Code, "method %s", method)
|
|
}
|
|
}
|