forked from casdoor/casdoor
feat: [mcp-5] add Application.Category and Application.Type fields for agent applications (MCP, A2A) (#5102)
This commit is contained in:
@@ -67,12 +67,21 @@ type JwtItem struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type ScopeItem struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
|
||||
Name string `xorm:"varchar(100) notnull pk" json:"name"`
|
||||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
|
||||
|
||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||
Category string `xorm:"varchar(20)" json:"category"`
|
||||
Type string `xorm:"varchar(20)" json:"type"`
|
||||
Scopes []*ScopeItem `xorm:"mediumtext" json:"scopes"`
|
||||
Logo string `xorm:"varchar(200)" json:"logo"`
|
||||
Title string `xorm:"varchar(100)" json:"title"`
|
||||
Favicon string `xorm:"varchar(200)" json:"favicon"`
|
||||
|
||||
@@ -198,6 +198,9 @@ func initBuiltInApplication() {
|
||||
Name: "app-built-in",
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
DisplayName: "Casdoor",
|
||||
Category: "Default",
|
||||
Type: "All",
|
||||
Scopes: []*ScopeItem{},
|
||||
Logo: fmt.Sprintf("%s/img/casdoor-logo_1185x256.png", conf.GetConfigString("staticBaseUrl")),
|
||||
HomepageUrl: "https://casdoor.org",
|
||||
Organization: "built-in",
|
||||
|
||||
@@ -133,6 +133,9 @@ func RegisterDynamicClient(req *DynamicClientRegistrationRequest, organization s
|
||||
Organization: organization,
|
||||
CreatedTime: createdTime,
|
||||
DisplayName: req.ClientName,
|
||||
Category: "Agent",
|
||||
Type: "MCP",
|
||||
Scopes: []*ScopeItem{},
|
||||
Logo: req.LogoUri,
|
||||
HomepageUrl: req.ClientUri,
|
||||
ClientId: clientId,
|
||||
|
||||
@@ -125,6 +125,23 @@ func GetOidcDiscovery(host string, applicationName string) OidcDiscovery {
|
||||
jwksUri = fmt.Sprintf("%s/.well-known/jwks", originBackend)
|
||||
}
|
||||
|
||||
// Default OIDC scopes
|
||||
scopes := []string{"openid", "email", "profile", "address", "phone", "offline_access"}
|
||||
|
||||
// Merge application-specific custom scopes if application is provided
|
||||
if applicationName != "" {
|
||||
applicationId := util.GetId("admin", applicationName)
|
||||
application, err := GetApplication(applicationId)
|
||||
if err == nil && application != nil && len(application.Scopes) > 0 {
|
||||
for _, scope := range application.Scopes {
|
||||
// Add custom scope names to the scopes list
|
||||
if scope.Name != "" {
|
||||
scopes = append(scopes, scope.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Examples:
|
||||
// https://login.okta.com/.well-known/openid-configuration
|
||||
// https://auth0.auth0.com/.well-known/openid-configuration
|
||||
@@ -144,7 +161,7 @@ func GetOidcDiscovery(host string, applicationName string) OidcDiscovery {
|
||||
GrantTypesSupported: []string{"authorization_code", "implicit", "password", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code", "urn:ietf:params:oauth:grant-type:token-exchange"},
|
||||
SubjectTypesSupported: []string{"public"},
|
||||
IdTokenSigningAlgValuesSupported: []string{"RS256", "RS512", "ES256", "ES384", "ES512"},
|
||||
ScopesSupported: []string{"openid", "email", "profile", "address", "phone", "offline_access"},
|
||||
ScopesSupported: scopes,
|
||||
CodeChallengeMethodsSupported: []string{"S256"},
|
||||
ClaimsSupported: []string{"iss", "ver", "sub", "aud", "iat", "exp", "id", "type", "displayName", "avatar", "permanentAvatar", "email", "phone", "location", "affiliation", "title", "homepage", "bio", "tag", "region", "language", "score", "ranking", "isOnline", "isAdmin", "isForbidden", "signupApplication", "ldap"},
|
||||
RequestParameterSupported: true,
|
||||
|
||||
@@ -48,6 +48,7 @@ import ProviderTable from "./table/ProviderTable";
|
||||
import SigninMethodTable from "./table/SigninMethodTable";
|
||||
import SignupTable from "./table/SignupTable";
|
||||
import SamlAttributeTable from "./table/SamlAttributeTable";
|
||||
import ScopeTable from "./table/ScopeTable";
|
||||
import PromptPage from "./auth/PromptPage";
|
||||
import copy from "copy-to-clipboard";
|
||||
import ThemeEditor from "./common/theme/ThemeEditor";
|
||||
@@ -307,6 +308,61 @@ class ApplicationEditPage extends React.Component {
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Category"), i18next.t("general:Category - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Select
|
||||
virtual={false}
|
||||
style={{width: "100%"}}
|
||||
value={this.state.application.category}
|
||||
onChange={(value) => {
|
||||
this.updateApplicationField("category", value);
|
||||
if (value === "Agent") {
|
||||
this.updateApplicationField("type", "MCP");
|
||||
} else {
|
||||
this.updateApplicationField("type", "All");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Option value="Default">Default</Option>
|
||||
<Option value="Agent">Agent</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Type"), i18next.t("general:Type - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<Select
|
||||
virtual={false}
|
||||
style={{width: "100%"}}
|
||||
value={this.state.application.type}
|
||||
onChange={(value) => {
|
||||
this.updateApplicationField("type", value);
|
||||
}}
|
||||
>
|
||||
{
|
||||
(this.state.application.category === "Agent") ? (
|
||||
<>
|
||||
<Option value="MCP">MCP</Option>
|
||||
<Option value="A2A">A2A</Option>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Option value="All">All</Option>
|
||||
<Option value="OIDC">OIDC</Option>
|
||||
<Option value="OAuth">OAuth</Option>
|
||||
<Option value="SAML">SAML</Option>
|
||||
<Option value="CAS">CAS</Option>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Is shared"), i18next.t("general:Is shared - Tooltip"))} :
|
||||
@@ -516,6 +572,22 @@ class ApplicationEditPage extends React.Component {
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
(this.state.application.category === "Agent") ? (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("general:Scopes"), i18next.t("general:Scopes - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={21} >
|
||||
<ScopeTable
|
||||
title={i18next.t("general:Scopes")}
|
||||
table={this.state.application.scopes}
|
||||
onUpdateTable={(value) => {this.updateApplicationField("scopes", value);}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
) : null
|
||||
}
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
|
||||
{Setting.getLabel(i18next.t("application:Token format"), i18next.t("application:Token format - Tooltip"))} :
|
||||
|
||||
@@ -38,6 +38,9 @@ class ApplicationListPage extends BaseListPage {
|
||||
organization: organizationName,
|
||||
createdTime: moment().format(),
|
||||
displayName: `New Application - ${randomName}`,
|
||||
category: "Default",
|
||||
type: "All",
|
||||
scopes: [],
|
||||
logo: `${Setting.StaticBaseUrl}/img/casdoor-logo_1185x256.png`,
|
||||
enablePassword: true,
|
||||
enableSignUp: true,
|
||||
@@ -179,6 +182,40 @@ class ApplicationListPage extends BaseListPage {
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("displayName"),
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Category"),
|
||||
dataIndex: "category",
|
||||
key: "category",
|
||||
width: "120px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("category"),
|
||||
render: (text, record, index) => {
|
||||
const category = text;
|
||||
const tagColor = category === "Agent" ? "green" : "blue";
|
||||
return (
|
||||
<span style={{
|
||||
padding: "4px 8px",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: tagColor,
|
||||
color: "white",
|
||||
fontWeight: "500",
|
||||
}}>
|
||||
{category}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Type"),
|
||||
dataIndex: "type",
|
||||
key: "type",
|
||||
width: "100px",
|
||||
sorter: true,
|
||||
...this.getColumnSearchProps("type"),
|
||||
render: (text, record, index) => {
|
||||
return text;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Logo",
|
||||
dataIndex: "logo",
|
||||
|
||||
166
web/src/table/ScopeTable.js
Normal file
166
web/src/table/ScopeTable.js
Normal file
@@ -0,0 +1,166 @@
|
||||
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import React from "react";
|
||||
import {DeleteOutlined, DownOutlined, UpOutlined} from "@ant-design/icons";
|
||||
import {Button, Input, Table, Tooltip} from "antd";
|
||||
import * as Setting from "../Setting";
|
||||
import i18next from "i18next";
|
||||
|
||||
class ScopeTable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
classes: props,
|
||||
};
|
||||
}
|
||||
|
||||
updateTable(table) {
|
||||
this.props.onUpdateTable(table);
|
||||
}
|
||||
|
||||
updateField(table, index, key, value) {
|
||||
table[index][key] = value;
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
addRow(table) {
|
||||
const row = {name: "", displayName: "", description: ""};
|
||||
if (table === undefined) {
|
||||
table = [];
|
||||
}
|
||||
table = Setting.addRow(table, row);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
deleteRow(table, i) {
|
||||
table = Setting.deleteRow(table, i);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
upRow(table, i) {
|
||||
table = Setting.swapRow(table, i - 1, i);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
downRow(table, i) {
|
||||
table = Setting.swapRow(table, i, i + 1);
|
||||
this.updateTable(table);
|
||||
}
|
||||
|
||||
renderTable(table) {
|
||||
if (table === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: i18next.t("general:Name"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
width: "25%",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input
|
||||
value={text}
|
||||
placeholder="e.g., files:read"
|
||||
onChange={e => {
|
||||
this.updateField(table, index, "name", e.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Display name"),
|
||||
dataIndex: "displayName",
|
||||
key: "displayName",
|
||||
width: "25%",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input
|
||||
value={text}
|
||||
placeholder="e.g., Read Files"
|
||||
onChange={e => {
|
||||
this.updateField(table, index, "displayName", e.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Description"),
|
||||
dataIndex: "description",
|
||||
key: "description",
|
||||
width: "40%",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<Input
|
||||
value={text}
|
||||
placeholder="e.g., Allow reading your files and documents"
|
||||
onChange={e => {
|
||||
this.updateField(table, index, "description", e.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: i18next.t("general:Action"),
|
||||
key: "action",
|
||||
width: "10%",
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Tooltip placement="bottomLeft" title={i18next.t("general:Up")}>
|
||||
<Button style={{marginRight: "5px"}} disabled={index === 0} icon={<UpOutlined />} size="small" onClick={() => this.upRow(table, index)} />
|
||||
</Tooltip>
|
||||
<Tooltip placement="topLeft" title={i18next.t("general:Down")}>
|
||||
<Button style={{marginRight: "5px"}} disabled={index === table.length - 1} icon={<DownOutlined />} size="small" onClick={() => this.downRow(table, index)} />
|
||||
</Tooltip>
|
||||
<Tooltip placement="topLeft" title={i18next.t("general:Delete")}>
|
||||
<Button icon={<DeleteOutlined />} size="small" onClick={() => this.deleteRow(table, index)} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table scroll={{x: "max-content"}} rowKey={(record, index) => index} columns={columns} dataSource={table} size="middle" bordered pagination={false}
|
||||
title={() => (
|
||||
<div>
|
||||
{this.props.title}
|
||||
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
this.renderTable(this.props.table)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ScopeTable;
|
||||
Reference in New Issue
Block a user