Skip to content

Commit 74b951f

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 60b08f0 commit 74b951f

File tree

10 files changed

+1743
-22
lines changed

10 files changed

+1743
-22
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/mcp/mcp.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+
if client == nil {
73+
return xerrors.New("client cannot be nil: MCP HTTP server requires authenticated client")
74+
}
75+
76+
// Create tool dependencies
77+
toolDeps, err := toolsdk.NewDeps(client)
78+
if err != nil {
79+
return xerrors.Errorf("failed to initialize tool dependencies: %w", err)
80+
}
81+
82+
// Register all available tools
83+
for _, tool := range toolsdk.All {
84+
s.mcpServer.AddTools(mcpFromSDK(tool, toolDeps))
85+
}
86+
87+
return nil
88+
}
89+
90+
// mcpFromSDK adapts a toolsdk.Tool to go-mcp's server.ServerTool
91+
func mcpFromSDK(sdkTool toolsdk.GenericTool, tb toolsdk.Deps) server.ServerTool {
92+
if sdkTool.Schema.Properties == nil {
93+
panic("developer error: schema properties cannot be nil")
94+
}
95+
96+
return server.ServerTool{
97+
Tool: mcp.Tool{
98+
Name: sdkTool.Name,
99+
Description: sdkTool.Description,
100+
InputSchema: mcp.ToolInputSchema{
101+
Type: "object",
102+
Properties: sdkTool.Schema.Properties,
103+
Required: sdkTool.Schema.Required,
104+
},
105+
},
106+
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
107+
var buf bytes.Buffer
108+
if err := json.NewEncoder(&buf).Encode(request.Params.Arguments); err != nil {
109+
return nil, xerrors.Errorf("failed to encode request arguments: %w", err)
110+
}
111+
result, err := sdkTool.Handler(ctx, tb, buf.Bytes())
112+
if err != nil {
113+
return nil, err
114+
}
115+
return &mcp.CallToolResult{
116+
Content: []mcp.Content{
117+
mcp.NewTextContent(string(result)),
118+
},
119+
}, nil
120+
},
121+
}
122+
}
123+
124+
// mcpLoggerAdapter adapts slog.Logger to the mcp-go util.Logger interface
125+
type mcpLoggerAdapter struct {
126+
logger slog.Logger
127+
}
128+
129+
func (l *mcpLoggerAdapter) Infof(format string, v ...any) {
130+
l.logger.Info(context.Background(), fmt.Sprintf(format, v...))
131+
}
132+
133+
func (l *mcpLoggerAdapter) Errorf(format string, v ...any) {
134+
l.logger.Error(context.Background(), fmt.Sprintf(format, v...))
135+
}

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