Skip to content

Commit 4ffc58e

Browse files
committed
feat(mcp): implement MCP HTTP server with toolsdk integration
- 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>
1 parent 8a3f7a9 commit 4ffc58e

File tree

7 files changed

+1523
-3
lines changed

7 files changed

+1523
-3
lines changed

coderd/coderd.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,10 @@ func New(options *Options) *API {
972972
r.Route("/aitasks", func(r chi.Router) {
973973
r.Get("/prompts", api.aiTasksPrompts)
974974
})
975+
r.Route("/mcp", func(r chi.Router) {
976+
// MCP HTTP transport endpoint with mandatory authentication
977+
r.Mount("/http", api.mcpHTTPHandler())
978+
})
975979
})
976980

977981
r.Route("/api/v2", func(r chi.Router) {

coderd/httpmw/apikey.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -653,8 +653,8 @@ func APITokenFromRequest(r *http.Request) string {
653653
// RFC 6750 Bearer Token support (added as fallback methods)
654654
// Check Authorization: Bearer <token> header
655655
authHeader := r.Header.Get("Authorization")
656-
if strings.HasPrefix(authHeader, "Bearer ") {
657-
return strings.TrimPrefix(authHeader, "Bearer ")
656+
if after, ok := strings.CutPrefix(authHeader, "Bearer "); ok {
657+
return after
658658
}
659659

660660
// Check access_token query parameter

coderd/mcp/mcp.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package mcp
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"net/http"
9+
"time"
10+
11+
"cdr.dev/slog"
12+
"github.com/mark3labs/mcp-go/mcp"
13+
"github.com/mark3labs/mcp-go/server"
14+
"golang.org/x/xerrors"
15+
16+
"github.com/coder/coder/v2/buildinfo"
17+
"github.com/coder/coder/v2/coderd/database"
18+
"github.com/coder/coder/v2/codersdk"
19+
"github.com/coder/coder/v2/codersdk/toolsdk"
20+
)
21+
22+
const (
23+
// MCPServerName is the name used for the MCP server.
24+
MCPServerName = "Coder"
25+
// MCPServerInstructions is the instructions text for the MCP server.
26+
MCPServerInstructions = "Coder MCP Server providing workspace and template management tools"
27+
)
28+
29+
// Server represents an MCP HTTP server instance
30+
type Server struct {
31+
Logger slog.Logger
32+
33+
// mcpServer is the underlying MCP server
34+
mcpServer *server.MCPServer
35+
36+
// streamableServer handles HTTP transport
37+
streamableServer *server.StreamableHTTPServer
38+
}
39+
40+
// NewServer creates a new MCP HTTP server
41+
func NewServer(db database.Store, logger slog.Logger) (*Server, error) {
42+
// Create the core MCP server
43+
mcpSrv := server.NewMCPServer(
44+
MCPServerName,
45+
buildinfo.Version(),
46+
server.WithInstructions(MCPServerInstructions),
47+
)
48+
49+
// Create logger adapter for mcp-go
50+
mcpLogger := &mcpLoggerAdapter{logger: logger}
51+
52+
// Create streamable HTTP server with configuration
53+
streamableServer := server.NewStreamableHTTPServer(mcpSrv,
54+
server.WithHeartbeatInterval(30*time.Second),
55+
server.WithLogger(mcpLogger),
56+
)
57+
58+
return &Server{
59+
Logger: logger,
60+
mcpServer: mcpSrv,
61+
streamableServer: streamableServer,
62+
}, nil
63+
}
64+
65+
// ServeHTTP implements http.Handler interface
66+
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
67+
s.streamableServer.ServeHTTP(w, r)
68+
}
69+
70+
// RegisterTools registers all available MCP tools with the server
71+
func (s *Server) RegisterTools(client *codersdk.Client) error {
72+
// Create tool dependencies
73+
toolDeps, err := toolsdk.NewDeps(client)
74+
if err != nil {
75+
return xerrors.Errorf("failed to initialize tool dependencies: %w", err)
76+
}
77+
78+
// Register all available tools
79+
for _, tool := range toolsdk.All {
80+
// Skip user-dependent tools if no authenticated client
81+
if !tool.UserClientOptional && client == nil {
82+
s.Logger.Warn(context.Background(), "Tool requires authentication and will not be available", slog.F("tool", tool.Name))
83+
continue
84+
}
85+
86+
s.mcpServer.AddTools(mcpFromSDK(tool, toolDeps))
87+
}
88+
89+
return nil
90+
}
91+
92+
// mcpFromSDK adapts a toolsdk.Tool to go-mcp's server.ServerTool
93+
func mcpFromSDK(sdkTool toolsdk.GenericTool, tb toolsdk.Deps) server.ServerTool {
94+
if sdkTool.Schema.Properties == nil {
95+
panic("developer error: schema properties cannot be nil")
96+
}
97+
98+
return server.ServerTool{
99+
Tool: mcp.Tool{
100+
Name: sdkTool.Name,
101+
Description: sdkTool.Description,
102+
InputSchema: mcp.ToolInputSchema{
103+
Type: "object",
104+
Properties: sdkTool.Schema.Properties,
105+
Required: sdkTool.Schema.Required,
106+
},
107+
},
108+
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
109+
var buf bytes.Buffer
110+
if err := json.NewEncoder(&buf).Encode(request.Params.Arguments); err != nil {
111+
return nil, xerrors.Errorf("failed to encode request arguments: %w", err)
112+
}
113+
result, err := sdkTool.Handler(ctx, tb, buf.Bytes())
114+
if err != nil {
115+
return nil, err
116+
}
117+
return &mcp.CallToolResult{
118+
Content: []mcp.Content{
119+
mcp.NewTextContent(string(result)),
120+
},
121+
}, nil
122+
},
123+
}
124+
}
125+
126+
// mcpLoggerAdapter adapts slog.Logger to the mcp-go util.Logger interface
127+
type mcpLoggerAdapter struct {
128+
logger slog.Logger
129+
}
130+
131+
func (l *mcpLoggerAdapter) Infof(format string, v ...any) {
132+
l.logger.Info(context.Background(), fmt.Sprintf(format, v...))
133+
}
134+
135+
func (l *mcpLoggerAdapter) Errorf(format string, v ...any) {
136+
l.logger.Error(context.Background(), fmt.Sprintf(format, v...))
137+
}

0 commit comments

Comments
 (0)