forked from casdoor/casdoor
feat: enhance MCP Permissions and Response Workflow, fix bugs (#4767)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
55
mcp/base.go
55
mcp/base.go
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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, ¶ms)
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user