feat: enhance MCP Permissions and Response Workflow, fix bugs (#4767)

This commit is contained in:
IsAurora6
2026-01-05 22:54:12 +08:00
committed by GitHub
parent e28344f0e7
commit 3bec49f16c
5 changed files with 93 additions and 84 deletions

View File

@@ -132,13 +132,19 @@ p, *, *, GET, /api/faceid-signin-begin, *, *
}
}
func IsAllowed(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
func IsAllowed(subOwner string, subName string, method string, urlPath string, objOwner string, objName string, extraInfo map[string]interface{}) bool {
if conf.IsDemoMode() {
if !isAllowedInDemoMode(subOwner, subName, method, urlPath, objOwner, objName) {
return false
}
}
if urlPath == "/api/mcp" {
if detailPath, ok := extraInfo["detailPathUrl"].(string); ok && detailPath != "tools/call" {
return true
}
}
user, err := object.GetUser(util.GetId(subOwner, subName))
if err != nil {
panic(err)
@@ -179,7 +185,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if method == "POST" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/sso-logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" || urlPath == "/api/refresh-engines" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/sso-logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" || urlPath == "/api/refresh-engines" || urlPath == "/api/mcp" {
return true
} else if urlPath == "/api/update-user" {
// Allow ordinary users to update their own information

View File

@@ -23,7 +23,7 @@ import (
)
// handleGetApplicationsTool handles the get_applications MCP tool
func (c *McpController) handleGetApplicationsTool(id string, args GetApplicationsArgs) {
func (c *McpController) handleGetApplicationsTool(id interface{}, args GetApplicationsArgs) {
userId := c.GetSessionUsername()
applications, err := object.GetApplications(args.Owner)
@@ -37,7 +37,7 @@ func (c *McpController) handleGetApplicationsTool(id string, args GetApplication
}
// handleGetApplicationTool handles the get_application MCP tool
func (c *McpController) handleGetApplicationTool(id string, args GetApplicationArgs) {
func (c *McpController) handleGetApplicationTool(id interface{}, args GetApplicationArgs) {
userId := c.GetSessionUsername()
application, err := object.GetApplication(args.Id)
@@ -51,7 +51,7 @@ func (c *McpController) handleGetApplicationTool(id string, args GetApplicationA
}
// handleAddApplicationTool handles the add_application MCP tool
func (c *McpController) handleAddApplicationTool(id string, args AddApplicationArgs) {
func (c *McpController) handleAddApplicationTool(id interface{}, args AddApplicationArgs) {
count, err := object.GetApplicationCount("", "", "")
if err != nil {
c.SendToolErrorResult(id, err.Error())
@@ -78,7 +78,7 @@ func (c *McpController) handleAddApplicationTool(id string, args AddApplicationA
}
// handleUpdateApplicationTool handles the update_application MCP tool
func (c *McpController) handleUpdateApplicationTool(id string, args UpdateApplicationArgs) {
func (c *McpController) handleUpdateApplicationTool(id interface{}, args UpdateApplicationArgs) {
if err := object.CheckIpWhitelist(args.Application.IpWhitelist, c.GetAcceptLanguage()); err != nil {
c.SendToolErrorResult(id, err.Error())
return
@@ -94,7 +94,7 @@ func (c *McpController) handleUpdateApplicationTool(id string, args UpdateApplic
}
// handleDeleteApplicationTool handles the delete_application MCP tool
func (c *McpController) handleDeleteApplicationTool(id string, args DeleteApplicationArgs) {
func (c *McpController) handleDeleteApplicationTool(id interface{}, args DeleteApplicationArgs) {
affected, err := object.DeleteApplication(&args.Application)
if err != nil {
c.SendToolErrorResult(id, err.Error())

View File

@@ -102,11 +102,11 @@ type DeleteApplicationArgs struct {
}
type McpCallToolResult struct {
Content []McpContent `json:"content"`
IsError bool `json:"isError,omitempty"`
Content []TextContent `json:"content"`
IsError bool `json:"isError,omitempty"`
}
type McpContent struct {
type TextContent struct {
Type string `json:"type"`
Text string `json:"text"`
}
@@ -116,6 +116,10 @@ type McpController struct {
web.Controller
}
func (c *McpController) Prepare() {
c.EnableRender = false
}
// SendMcpResponse sends a successful MCP response
func (c *McpController) SendMcpResponse(id interface{}, result interface{}) {
resp := McpResponse{
@@ -123,6 +127,9 @@ func (c *McpController) SendMcpResponse(id interface{}, result interface{}) {
ID: id,
Result: result,
}
// Set proper HTTP headers for MCP responses
c.Ctx.Output.Header("Content-Type", "application/json")
c.Data["json"] = resp
c.ServeJSON()
}
@@ -138,6 +145,9 @@ func (c *McpController) SendMcpError(id interface{}, code int, message string, d
Data: data,
},
}
// Set proper HTTP headers for MCP responses
c.Ctx.Output.Header("Content-Type", "application/json")
c.Data["json"] = resp
c.ServeJSON()
}
@@ -150,13 +160,12 @@ func (c *McpController) sendInvalidParamsError(id interface{}, details string) {
// SendToolResult sends a successful tool execution result
func (c *McpController) SendToolResult(id interface{}, text string) {
result := McpCallToolResult{
Content: []McpContent{
Content: []TextContent{
{
Type: "text",
Text: text,
},
},
IsError: false,
}
c.SendMcpResponse(id, result)
}
@@ -164,7 +173,7 @@ func (c *McpController) SendToolResult(id interface{}, text string) {
// SendToolErrorResult sends a tool execution error result
func (c *McpController) SendToolErrorResult(id interface{}, errorMsg string) {
result := McpCallToolResult{
Content: []McpContent{
Content: []TextContent{
{
Type: "text",
Text: errorMsg,
@@ -210,6 +219,10 @@ func (c *McpController) HandleMcp() {
switch req.Method {
case "initialize":
c.handleInitialize(req)
case "notifications/initialized":
c.handleNotificationsInitialized(req)
case "ping":
c.handlePing(req)
case "tools/list":
c.handleToolsList(req)
case "tools/call":
@@ -232,7 +245,9 @@ func (c *McpController) handleInitialize(req McpRequest) {
result := McpInitializeResult{
ProtocolVersion: "2024-11-05",
Capabilities: McpServerCapabilities{
Tools: map[string]interface{}{},
Tools: map[string]interface{}{
"listChanged": true,
},
},
ServerInfo: McpImplementation{
Name: "Casdoor MCP Server",
@@ -243,6 +258,19 @@ func (c *McpController) handleInitialize(req McpRequest) {
c.SendMcpResponse(req.ID, result)
}
func (c *McpController) handleNotificationsInitialized(req McpRequest) {
// notifications/initialized is a notification from client indicating
// that the initialization process is complete and the client is ready
// to start using the server. No response is expected for notifications.
// We can log this event or perform any post-initialization setup here.
}
func (c *McpController) handlePing(req McpRequest) {
// ping method is used to check if the server is alive and responsive
// Return an empty object as result to indicate server is active
c.SendMcpResponse(req.ID, map[string]interface{}{})
}
func (c *McpController) handleToolsList(req McpRequest) {
tools := []McpTool{
{
@@ -336,9 +364,6 @@ func (c *McpController) handleToolsCall(req McpRequest) {
return
}
// Convert ID to string for tool handlers
idStr := fmt.Sprintf("%v", req.ID)
// Route to the appropriate tool handler
switch params.Name {
case "get_applications":
@@ -347,35 +372,35 @@ func (c *McpController) handleToolsCall(req McpRequest) {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleGetApplicationsTool(idStr, args)
c.handleGetApplicationsTool(req.ID, args)
case "get_application":
var args GetApplicationArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleGetApplicationTool(idStr, args)
c.handleGetApplicationTool(req.ID, args)
case "add_application":
var args AddApplicationArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleAddApplicationTool(idStr, args)
c.handleAddApplicationTool(req.ID, args)
case "update_application":
var args UpdateApplicationArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleUpdateApplicationTool(idStr, args)
c.handleUpdateApplicationTool(req.ID, args)
case "delete_application":
var args DeleteApplicationArgs
if err := json.Unmarshal(params.Arguments, &args); err != nil {
c.sendInvalidParamsError(req.ID, err.Error())
return
}
c.handleDeleteApplicationTool(idStr, args)
c.handleDeleteApplicationTool(req.ID, args)
default:
c.SendMcpError(req.ID, -32602, "Invalid tool name", fmt.Sprintf("Tool '%s' not found", params.Name))
}

View File

@@ -212,11 +212,8 @@ func willLog(subOwner string, subName string, method string, urlPath string, obj
return true
}
func getUrlPath(urlPath string, ctx *context.Context) string {
// Special handling for MCP requests
if urlPath == "/api/mcp" {
return getMcpUrlPath(ctx)
}
func getUrlPath(ctx *context.Context) string {
urlPath := ctx.Request.URL.Path
if strings.HasPrefix(urlPath, "/cas") && (strings.HasSuffix(urlPath, "/serviceValidate") || strings.HasSuffix(urlPath, "/proxy") || strings.HasSuffix(urlPath, "/proxyValidate") || strings.HasSuffix(urlPath, "/validate") || strings.HasSuffix(urlPath, "/p3/serviceValidate") || strings.HasSuffix(urlPath, "/p3/proxyValidate") || strings.HasSuffix(urlPath, "/samlValidate")) {
return "/cas"
@@ -241,10 +238,31 @@ func getUrlPath(urlPath string, ctx *context.Context) string {
return urlPath
}
func getExtraInfo(ctx *context.Context, urlPath string) map[string]interface{} {
var extra map[string]interface{}
if urlPath == "/api/mcp" {
var m map[string]interface{}
if err := json.Unmarshal(ctx.Input.RequestBody, &m); err != nil {
return nil
}
method, ok := m["method"].(string)
if !ok {
return nil
}
return map[string]interface{}{
"detailPathUrl": method,
}
}
return extra
}
func ApiFilter(ctx *context.Context) {
subOwner, subName := getSubject(ctx)
method := ctx.Request.Method
urlPath := getUrlPath(ctx.Request.URL.Path, ctx)
urlPath := getUrlPath(ctx)
extraInfo := getExtraInfo(ctx, urlPath)
objOwner, objName := "", ""
if urlPath != "/api/get-app-login" && urlPath != "/api/get-resource" {
@@ -260,7 +278,7 @@ func ApiFilter(ctx *context.Context) {
urlPath = "/api/notify-payment"
}
isAllowed := authz.IsAllowed(subOwner, subName, method, urlPath, objOwner, objName)
isAllowed := authz.IsAllowed(subOwner, subName, method, urlPath, objOwner, objName, extraInfo)
result := "deny"
if isAllowed {
@@ -270,6 +288,10 @@ func ApiFilter(ctx *context.Context) {
if willLog(subOwner, subName, method, urlPath, objOwner, objName) {
logLine := fmt.Sprintf("subOwner = %s, subName = %s, method = %s, urlPath = %s, obj.Owner = %s, obj.Name = %s, result = %s",
subOwner, subName, method, urlPath, objOwner, objName, result)
extra := formatExtraInfo(extraInfo)
if extra != "" {
logLine += fmt.Sprintf(", extraInfo = %s", extra)
}
fmt.Println(logLine)
util.LogInfo(ctx, logLine)
}
@@ -290,3 +312,14 @@ func ApiFilter(ctx *context.Context) {
})
}
}
func formatExtraInfo(extra map[string]interface{}) string {
if extra == nil {
return ""
}
b, err := json.Marshal(extra)
if err != nil {
return ""
}
return string(b)
}

View File

@@ -127,58 +127,3 @@ func extractOwnerNameFromAppStub(app applicationStub) (string, string, error) {
}
return "", "", nil
}
func getMcpUrlPath(ctx *context.Context) string {
body := ctx.Input.RequestBody
if len(body) == 0 {
return "/api/mcp"
}
type mcpRequest struct {
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
type mcpCallToolParams struct {
Name string `json:"name"`
}
var mcpReq mcpRequest
err := json.Unmarshal(body, &mcpReq)
if err != nil {
return "/api/mcp"
}
// Map initialize and tools/list to public endpoints
// These operations don't require special permissions beyond authentication
// We use /api/get-application as it's a read-only operation that authenticated users can access
if mcpReq.Method == "initialize" || mcpReq.Method == "tools/list" {
return "/api/get-application"
}
if mcpReq.Method != "tools/call" {
return "/api/mcp"
}
var params mcpCallToolParams
err = json.Unmarshal(mcpReq.Params, &params)
if err != nil {
return "/api/mcp"
}
// Map MCP tool names to corresponding API paths
switch params.Name {
case "get_applications":
return "/api/get-applications"
case "get_application":
return "/api/get-application"
case "add_application":
return "/api/add-application"
case "update_application":
return "/api/update-application"
case "delete_application":
return "/api/delete-application"
default:
return "/api/mcp"
}
}