b1b28d72a3
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
92 lines
3.0 KiB
Go
92 lines
3.0 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
mcporigin "github.com/kandev/kandev/internal/mcp/origin"
|
|
ws "github.com/kandev/kandev/pkg/websocket"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Dispatcher is the subset of *ws.Dispatcher used by DispatcherBackendClient.
|
|
// Defined as an interface so tests can supply lightweight fakes.
|
|
type Dispatcher interface {
|
|
Dispatch(ctx context.Context, msg *ws.Message) (*ws.Message, error)
|
|
}
|
|
|
|
// DispatcherBackendClient implements BackendClient by calling a ws.Dispatcher
|
|
// in-process — no channels, no WebSocket round-trip. Used by the backend's
|
|
// external MCP endpoint where handlers and MCP server live in the same process.
|
|
type DispatcherBackendClient struct {
|
|
dispatcher Dispatcher
|
|
logger *logger.Logger
|
|
trustedExternalTransport bool
|
|
}
|
|
|
|
// NewExternalDispatcherBackendClient creates the trusted bridge used only by
|
|
// the authenticated external MCP endpoint.
|
|
func NewExternalDispatcherBackendClient(d Dispatcher, log *logger.Logger) *DispatcherBackendClient {
|
|
client := NewDispatcherBackendClient(d, log)
|
|
client.trustedExternalTransport = true
|
|
return client
|
|
}
|
|
|
|
// NewDispatcherBackendClient creates a BackendClient backed by a ws.Dispatcher.
|
|
func NewDispatcherBackendClient(d Dispatcher, log *logger.Logger) *DispatcherBackendClient {
|
|
clientLogger := logger.Default()
|
|
if log != nil {
|
|
clientLogger = log
|
|
}
|
|
return &DispatcherBackendClient{
|
|
dispatcher: d,
|
|
logger: clientLogger.WithFields(zap.String("component", "mcp-dispatcher-backend-client")),
|
|
}
|
|
}
|
|
|
|
// RequestPayload dispatches the request to the in-process handler and unmarshals the response.
|
|
func (c *DispatcherBackendClient) RequestPayload(ctx context.Context, action string, payload, result interface{}) error {
|
|
if c.trustedExternalTransport {
|
|
ctx = mcporigin.WithTrustedExternalTransport(ctx)
|
|
}
|
|
id := uuid.New().String()
|
|
msg, err := ws.NewRequest(id, action, payload)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
resp, err := c.dispatcher.Dispatch(ctx, msg)
|
|
if err != nil {
|
|
return fmt.Errorf("dispatch failed for %s: %w", action, err)
|
|
}
|
|
if resp == nil {
|
|
return fmt.Errorf("dispatcher returned nil response for %s", action)
|
|
}
|
|
if resp.Type == ws.MessageTypeError {
|
|
var ep struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
if json.Unmarshal(resp.Payload, &ep) == nil {
|
|
return fmt.Errorf("backend error [%s]: %s", ep.Code, ep.Message)
|
|
}
|
|
return fmt.Errorf("backend error: %s", string(resp.Payload))
|
|
}
|
|
if result == nil {
|
|
return nil
|
|
}
|
|
if len(resp.Payload) == 0 {
|
|
c.logger.Warn(ErrEmptyBackendPayload.Error(),
|
|
zap.String("request_id", id),
|
|
zap.String("action", action))
|
|
return fmt.Errorf("empty response payload for action %q: %w", action, ErrEmptyBackendPayload)
|
|
}
|
|
if err := json.Unmarshal(resp.Payload, result); err != nil {
|
|
return fmt.Errorf("failed to unmarshal response for %s: %w", action, err)
|
|
}
|
|
return nil
|
|
}
|