mirror of
https://github.com/coder/coder.git
synced 2026-06-03 21:18:24 +00:00
bddb808b25
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example: ``` import ( "context" "time" "github.com/prometheus/client_golang/prometheus" "golang.org/x/xerrors" "gopkg.in/natefinch/lumberjack.v2" "cdr.dev/slog/v3" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/serpent" ) ``` 3 groups: standard library, 3rd partly libs, Coder libs. This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
170 lines
5.0 KiB
Go
170 lines
5.0 KiB
Go
package mcp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/mark3labs/mcp-go/server"
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"github.com/coder/coder/v2/buildinfo"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/codersdk/toolsdk"
|
|
)
|
|
|
|
const (
|
|
// MCPServerName is the name used for the MCP server.
|
|
MCPServerName = "Coder"
|
|
// MCPServerInstructions is the instructions text for the MCP server.
|
|
MCPServerInstructions = "Coder MCP Server providing workspace and template management tools"
|
|
|
|
// Used in tests and aibridge.
|
|
MCPEndpoint = "/api/experimental/mcp/http"
|
|
)
|
|
|
|
// Server represents an MCP HTTP server instance
|
|
type Server struct {
|
|
Logger slog.Logger
|
|
|
|
// mcpServer is the underlying MCP server
|
|
mcpServer *server.MCPServer
|
|
|
|
// streamableServer handles HTTP transport
|
|
streamableServer *server.StreamableHTTPServer
|
|
}
|
|
|
|
// NewServer creates a new MCP HTTP server
|
|
func NewServer(logger slog.Logger) (*Server, error) {
|
|
// Create the core MCP server
|
|
mcpSrv := server.NewMCPServer(
|
|
MCPServerName,
|
|
buildinfo.Version(),
|
|
server.WithInstructions(MCPServerInstructions),
|
|
)
|
|
|
|
// Create logger adapter for mcp-go
|
|
mcpLogger := &mcpLoggerAdapter{logger: logger}
|
|
|
|
// Create streamable HTTP server with configuration
|
|
streamableServer := server.NewStreamableHTTPServer(mcpSrv,
|
|
server.WithHeartbeatInterval(30*time.Second),
|
|
server.WithLogger(mcpLogger),
|
|
)
|
|
|
|
return &Server{
|
|
Logger: logger,
|
|
mcpServer: mcpSrv,
|
|
streamableServer: streamableServer,
|
|
}, nil
|
|
}
|
|
|
|
// ServeHTTP implements http.Handler interface
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
s.streamableServer.ServeHTTP(w, r)
|
|
}
|
|
|
|
// Register all available MCP tools with the server excluding:
|
|
// - ReportTask - which requires dependencies not available in the remote MCP context
|
|
// - ChatGPT search and fetch tools, which are redundant with the standard tools.
|
|
func (s *Server) RegisterTools(client *codersdk.Client) error {
|
|
if client == nil {
|
|
return xerrors.New("client cannot be nil: MCP HTTP server requires authenticated client")
|
|
}
|
|
|
|
// Create tool dependencies
|
|
toolDeps, err := toolsdk.NewDeps(client)
|
|
if err != nil {
|
|
return xerrors.Errorf("failed to initialize tool dependencies: %w", err)
|
|
}
|
|
|
|
for _, tool := range toolsdk.All {
|
|
// the ReportTask tool requires dependencies not available in the remote MCP context
|
|
// the ChatGPT search and fetch tools are redundant with the standard tools.
|
|
if tool.Name == toolsdk.ToolNameReportTask ||
|
|
tool.Name == toolsdk.ToolNameChatGPTSearch || tool.Name == toolsdk.ToolNameChatGPTFetch {
|
|
continue
|
|
}
|
|
|
|
s.mcpServer.AddTools(mcpFromSDK(tool, toolDeps))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ChatGPT tools are the search and fetch tools as defined in https://platform.openai.com/docs/mcp.
|
|
// We do not expose any extra ones because ChatGPT has an undocumented "Safety Scan" feature.
|
|
// In my experiments, if I included extra tools in the MCP server, ChatGPT would often - but not always -
|
|
// refuse to add Coder as a connector.
|
|
func (s *Server) RegisterChatGPTTools(client *codersdk.Client) error {
|
|
if client == nil {
|
|
return xerrors.New("client cannot be nil: MCP HTTP server requires authenticated client")
|
|
}
|
|
|
|
// Create tool dependencies
|
|
toolDeps, err := toolsdk.NewDeps(client)
|
|
if err != nil {
|
|
return xerrors.Errorf("failed to initialize tool dependencies: %w", err)
|
|
}
|
|
|
|
for _, tool := range toolsdk.All {
|
|
if tool.Name != toolsdk.ToolNameChatGPTSearch && tool.Name != toolsdk.ToolNameChatGPTFetch {
|
|
continue
|
|
}
|
|
|
|
s.mcpServer.AddTools(mcpFromSDK(tool, toolDeps))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// mcpFromSDK adapts a toolsdk.Tool to go-mcp's server.ServerTool
|
|
func mcpFromSDK(sdkTool toolsdk.GenericTool, tb toolsdk.Deps) server.ServerTool {
|
|
if sdkTool.Schema.Properties == nil {
|
|
panic("developer error: schema properties cannot be nil")
|
|
}
|
|
|
|
return server.ServerTool{
|
|
Tool: mcp.Tool{
|
|
Name: sdkTool.Name,
|
|
Description: sdkTool.Description,
|
|
InputSchema: mcp.ToolInputSchema{
|
|
Type: "object",
|
|
Properties: sdkTool.Schema.Properties,
|
|
Required: sdkTool.Schema.Required,
|
|
},
|
|
},
|
|
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
var buf bytes.Buffer
|
|
if err := json.NewEncoder(&buf).Encode(request.Params.Arguments); err != nil {
|
|
return nil, xerrors.Errorf("failed to encode request arguments: %w", err)
|
|
}
|
|
result, err := sdkTool.Handler(ctx, tb, buf.Bytes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
mcp.NewTextContent(string(result)),
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// mcpLoggerAdapter adapts slog.Logger to the mcp-go util.Logger interface
|
|
type mcpLoggerAdapter struct {
|
|
logger slog.Logger
|
|
}
|
|
|
|
func (l *mcpLoggerAdapter) Infof(format string, v ...any) {
|
|
l.logger.Info(context.Background(), fmt.Sprintf(format, v...))
|
|
}
|
|
|
|
func (l *mcpLoggerAdapter) Errorf(format string, v ...any) {
|
|
l.logger.Error(context.Background(), fmt.Sprintf(format, v...))
|
|
}
|