Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var RouteProvider = wire.NewSet(
chat.NewCompletionAPI,
conv_chat.NewConvChatRoute,
conv_chat.NewConvCompletionAPI,
conv_chat.NewConvMCPAPI,
conv_chat.NewCompletionNonStreamHandler,
conv_chat.NewCompletionStreamHandler,
mcp.NewMCPAPI,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@ import (
type ConvChatRoute struct {
authService *auth.AuthService
convCompletionAPI *ConvCompletionAPI
convMCPAPI *ConvMCPAPI
}

// NewConvChatRoute creates a new conversation-aware chat route handler
func NewConvChatRoute(
authService *auth.AuthService,
convCompletionAPI *ConvCompletionAPI,
convMCPAPI *ConvMCPAPI,
) *ConvChatRoute {
return &ConvChatRoute{
authService: authService,
convCompletionAPI: convCompletionAPI,
convMCPAPI: convMCPAPI,
}
}

Expand All @@ -33,4 +36,7 @@ func (convChatRoute *ConvChatRoute) RegisterRouter(router gin.IRouter) {
convChatRoute.authService.RegisteredUserMiddleware(),
)
convChatRoute.convCompletionAPI.RegisterRouter(convChatRouter)

// Register MCP routes separately (without RegisteredUserMiddleware to avoid content type conflicts)
convChatRoute.convMCPAPI.RegisterRouter(convChatRouter)
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
package conv

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"

"github.com/gin-gonic/gin"
mcpserver "github.com/mark3labs/mcp-go/server"
openai "github.com/sashabaranov/go-openai"
"menlo.ai/jan-api-gateway/app/domain/auth"
"menlo.ai/jan-api-gateway/app/domain/common"
"menlo.ai/jan-api-gateway/app/domain/conversation"
inferencemodelregistry "menlo.ai/jan-api-gateway/app/domain/inference_model_registry"
userdomain "menlo.ai/jan-api-gateway/app/domain/user"
"menlo.ai/jan-api-gateway/app/interfaces/http/responses"
mcpimpl "menlo.ai/jan-api-gateway/app/interfaces/http/routes/v1/mcp/mcp_impl"
"menlo.ai/jan-api-gateway/app/utils/idgen"
"menlo.ai/jan-api-gateway/app/utils/logger"
)
Expand All @@ -33,97 +29,24 @@ type ConvCompletionAPI struct {
completionStreamHandler *CompletionStreamHandler
conversationService *conversation.ConversationService
authService *auth.AuthService
serperMCP *mcpimpl.SerperMCP
mcpServer *mcpserver.MCPServer
}

func NewConvCompletionAPI(completionNonStreamHandler *CompletionNonStreamHandler, completionStreamHandler *CompletionStreamHandler, conversationService *conversation.ConversationService, authService *auth.AuthService, serperMCP *mcpimpl.SerperMCP) *ConvCompletionAPI {
mcpSrv := mcpserver.NewMCPServer("conv-demo", "0.1.0",
mcpserver.WithToolCapabilities(true),
mcpserver.WithRecovery(),
)
func NewConvCompletionAPI(completionNonStreamHandler *CompletionNonStreamHandler, completionStreamHandler *CompletionStreamHandler, conversationService *conversation.ConversationService, authService *auth.AuthService) *ConvCompletionAPI {
return &ConvCompletionAPI{
completionNonStreamHandler: completionNonStreamHandler,
completionStreamHandler: completionStreamHandler,
conversationService: conversationService,
authService: authService,
serperMCP: serperMCP,
mcpServer: mcpSrv,
}
}

// ConvMCP
// @Summary MCP streamable endpoint for conversation-aware chat
// @Description Handles Model Context Protocol (MCP) requests over an HTTP stream for conversation-aware chat functionality. The response is sent as a continuous stream of data with conversation context.
// @Tags Conversation-aware Chat API
// @Security BearerAuth
// @Accept json
// @Produce text/event-stream
// @Param request body any true "MCP request payload"
// @Success 200 {string} string "Streamed response (SSE or chunked transfer)"
// @Router /v1/conv/mcp [post]
func (completionAPI *ConvCompletionAPI) RegisterRouter(router *gin.RouterGroup) {
// Register chat completions under /chat subroute
chatRouter := router.Group("/chat")
chatRouter.POST("/completions", completionAPI.PostCompletion)

// Register other endpoints at root level
router.GET("/models", completionAPI.GetModels)

// Register MCP endpoint
completionAPI.serperMCP.RegisterTool(completionAPI.mcpServer)
mcpHttpHandler := mcpserver.NewStreamableHTTPServer(completionAPI.mcpServer)
router.Any(
"/mcp",
completionAPI.authService.AppUserAuthMiddleware(),
MCPMethodGuard(map[string]bool{
// Initialization / handshake
"initialize": true,
"notifications/initialized": true,
"ping": true,

// Tools
"tools/call": true,

// Prompts
"prompts/list": true,
"prompts/call": true,

// Resources
"resources/list": true,
"resources/templates/list": true,
"resources/read": true,

// If you support subscription:
"resources/subscribe": true,
}),
gin.WrapH(mcpHttpHandler))
}

// MCPMethodGuard is a middleware that guards MCP methods
func MCPMethodGuard(allowedMethods map[string]bool) gin.HandlerFunc {
return func(c *gin.Context) {
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
c.Abort()
return
}
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
var req struct {
Method string `json:"method"`
}

if err := json.Unmarshal(bodyBytes, &req); err != nil {
c.Abort()
return
}

if !allowedMethods[req.Method] {
c.Abort()
return
}
c.Next()
}
}

// ExtendedChatCompletionRequest extends OpenAI's request with conversation field and store and store_reasoning fields
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package conv

import (
"bytes"
"encoding/json"
"io"

"github.com/gin-gonic/gin"
mcpserver "github.com/mark3labs/mcp-go/server"
"menlo.ai/jan-api-gateway/app/domain/auth"
mcpimpl "menlo.ai/jan-api-gateway/app/interfaces/http/routes/v1/mcp/mcp_impl"
)

// ConvMCPAPI handles MCP (Model Context Protocol) endpoints for conversation-aware chat
type ConvMCPAPI struct {
authService *auth.AuthService
serperMCP *mcpimpl.SerperMCP
mcpServer *mcpserver.MCPServer
}

// NewConvMCPAPI creates a new ConvMCPAPI instance
func NewConvMCPAPI(authService *auth.AuthService, serperMCP *mcpimpl.SerperMCP) *ConvMCPAPI {
mcpSrv := mcpserver.NewMCPServer("conv-mcp-demo", "0.1.0",
mcpserver.WithToolCapabilities(true),
mcpserver.WithRecovery(),
)
return &ConvMCPAPI{
authService: authService,
serperMCP: serperMCP,
mcpServer: mcpSrv,
}
}

// RegisterRouter registers the MCP routes for conversation-aware chat
// ConvMCP
// @Summary MCP streamable endpoint for conversation-aware chat
// @Description Handles Model Context Protocol (MCP) requests over an HTTP stream for conversation-aware chat functionality. The response is sent as a continuous stream of data with conversation context.
// @Tags Conversation-aware Chat API
// @Security BearerAuth
// @Accept json
// @Produce text/event-stream
// @Param request body any true "MCP request payload"
// @Success 200 {string} string "Streamed response (SSE or chunked transfer)"
// @Router /v1/conv/mcp [post]
func (api *ConvMCPAPI) RegisterRouter(router *gin.RouterGroup) {
// Register MCP endpoint (without RegisteredUserMiddleware to avoid content type conflicts)
api.serperMCP.RegisterTool(api.mcpServer)
mcpHttpHandler := mcpserver.NewStreamableHTTPServer(api.mcpServer)

// Create a separate router group for MCP that only uses AppUserAuthMiddleware
// This avoids the content type conflicts that RegisteredUserMiddleware can cause
mcpRouter := router.Group("")
mcpRouter.Any(
"/mcp",
api.authService.AppUserAuthMiddleware(),
MCPMethodGuard(map[string]bool{
// Initialization / handshake
"initialize": true,
"notifications/initialized": true,
"ping": true,

// Tools
"tools/list": true,
"tools/call": true,

// Prompts
"prompts/list": true,
"prompts/call": true,

// Resources
"resources/list": true,
"resources/templates/list": true,
"resources/read": true,

// If you support subscription:
"resources/subscribe": true,
}),
gin.WrapH(mcpHttpHandler))
}

// MCPMethodGuard is a middleware that guards MCP methods
func MCPMethodGuard(allowedMethods map[string]bool) gin.HandlerFunc {
return func(c *gin.Context) {
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
c.Abort()
return
}
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
var req struct {
Method string `json:"method"`
}

if err := json.Unmarshal(bodyBytes, &req); err != nil {
c.Abort()
return
}

if !allowedMethods[req.Method] {
c.Abort()
return
}
c.Next()
}
}
5 changes: 3 additions & 2 deletions apps/jan-api-gateway/application/cmd/server/wire_gen.go

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

Loading