8000 [pull] main from coder:main by pull[bot] · Pull Request #162 · Klomgor/coder · GitHub
[go: up one dir, main page]

Skip to content

[pull] main from coder:main #162

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 3, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: implement MCP HTTP server endpoint with authentication (coder#1…
…8670)

# Add MCP HTTP server with streamable transport support

- Add MCP HTTP server with streamable transport support
- Integrate with existing toolsdk for Coder workspace operations
- Add comprehensive E2E tests with OAuth2 bearer token support
- Register MCP endpoint at /api/experimental/mcp/http with authentication
- Support RFC 6750 Bearer token authentication for MCP clients

Change-Id: Ib9024569ae452729908797c42155006aa04330af
Signed-off-by: Thomas Kosiewski <tk@coder.com>
  • Loading branch information
ThomasK33 authored Jul 3, 2025
commit 494dccc510250dc74b0967f1b67e42446c70268d
68 changes: 67 additions & 1 deletion coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 57 additions & 1 deletion coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,10 @@ func New(options *Options) *API {
r.Route("/aitasks", func(r chi.Router) {
r.Get("/prompts", api.aiTasksPrompts)
})
r.Route("/mcp", func(r chi.Router) {
// MCP HTTP transport endpoint with mandatory authentication
r.Mount("/http", api.mcpHTTPHandler())
})
})

r.Route("/api/v2", func(r chi.Router) {
Expand Down
135 changes: 135 additions & 0 deletions coderd/mcp/mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package mcp

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
8000
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"golang.org/x/xerrors"

"cdr.dev/slog"

"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"
)

// 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)
}

// RegisterTools registers all available MCP tools with the server
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)
}

// Register all available tools
for _, tool := range toolsdk.All {
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...))
}
Loading
0