Skip to content

Commit 06d9b57

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 10701da commit 06d9b57

File tree

10 files changed

+1686
-7
lines changed

10 files changed

+1686
-7
lines changed

coderd/apidoc/docs.go

Lines changed: 67 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 57 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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
@@ -766,8 +766,8 @@ func APITokenFromRequest(r *http.Request) string {
766766
// RFC 6750 Bearer Token support (added as fallback methods)
767767
// Check Authorization: Bearer <token> header
768768
authHeader := r.Header.Get("Authorization")
769-
if strings.HasPrefix(authHeader, "Bearer ") {
770-
return strings.TrimPrefix(authHeader, "Bearer ")
769+
if after, ok := strings.CutPrefix(authHeader, "Bearer "); ok {
770+
return after
771771
}
772772

773773
// 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+
"github.com/mark3labs/mcp-go/mcp"
12+
"github.com/mark3labs/mcp-go/server"
13+
"golang.org/x/xerrors"
14+
15+
"cdr.dev/slog"
16+
17+
"github.com/coder/coder/v2/buildinfo"
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(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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy