Skip to content
Closed
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
43 changes: 42 additions & 1 deletion mcp/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,48 @@ vi.mock("./utils/cloud-mode.js", () => ({
isCloudMode: vi.fn(() => false),
}));
vi.mock("./utils/tencent-cloud.js", () => ({ isInternationalRegion: vi.fn(() => false) }));
vi.mock("@modelcontextprotocol/sdk/types.js", () => ({ SetLevelRequestSchema: {} }));
vi.mock("@modelcontextprotocol/sdk/types.js", () => ({
ListToolsRequestSchema: { shape: { method: { value: "tools/list" } } },
SetLevelRequestSchema: { shape: { method: { value: "logging/setLevel" } } },
}));

describe("server tool schema overrides", () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
});

it("should require username and password in listTools schema when action=createUser", async () => {
const { applyManagePermissionsCreateUserRequirements } = await import("./server.js");

const result = applyManagePermissionsCreateUserRequirements({
type: "object",
properties: {
action: { type: "string" },
username: { type: "string" },
password: { type: "string" },
},
required: ["action"],
});

expect(result).toMatchObject({
required: ["action"],
allOf: [
{
if: {
properties: {
action: { const: "createUser" },
},
required: ["action"],
},
then: {
required: ["username", "password"],
},
},
],
});
});
});

describe("server plugin registration", () => {
beforeEach(() => {
Expand Down
84 changes: 83 additions & 1 deletion mcp/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { RegisteredTool } from "@modelcontextprotocol/sdk/server/mcp.js";
import { zodToJsonSchema } from "zod-to-json-schema";
import { registerDatabaseTools } from "./tools/databaseNoSQL.js";
import { registerSQLDatabaseTools } from "./tools/databaseSQL.js";
import { registerDownloadTools } from "./tools/download.js";
Expand All @@ -9,7 +11,7 @@ import { registerRagTools } from "./tools/rag.js";
import { registerSetupTools } from "./tools/setup.js";
import { registerStorageTools } from "./tools/storage.js";
// import { registerMiniprogramTools } from "./tools/miniprogram.js";
import { SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { ListToolsRequestSchema, SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { registerCapiTools } from "./tools/capi.js";
import { registerCloudRunTools } from "./tools/cloudrun.js";
import { registerDataModelTools } from "./tools/dataModel.js";
Expand All @@ -34,6 +36,84 @@ interface PluginDefinition {
register: (server: ExtendedMcpServer) => void | Promise<void>;
}

type JsonSchemaObject = {
type?: string;
properties?: Record<string, unknown>;
required?: string[];
allOf?: unknown[];
[key: string]: unknown;
};

const EMPTY_OBJECT_JSON_SCHEMA: JsonSchemaObject = {
type: "object",
properties: {},
};

export function applyManagePermissionsCreateUserRequirements(inputSchema: JsonSchemaObject): JsonSchemaObject {
if (
inputSchema?.type !== "object" ||
!inputSchema.properties?.action ||
!inputSchema.properties?.username ||
!inputSchema.properties?.password
) {
return inputSchema;
}

const existingAllOf = Array.isArray(inputSchema.allOf) ? inputSchema.allOf : [];

return {
...inputSchema,
allOf: [
...existingAllOf,
{
if: {
properties: {
action: { const: "createUser" },
},
required: ["action"],
},
then: {
required: ["username", "password"],
},
},
],
};
}

function overrideListToolsHandler(server: ExtendedMcpServer) {
const registeredTools = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
._registeredTools;

server.server.setRequestHandler(ListToolsRequestSchema, () => ({
tools: Object.entries(registeredTools)
.filter(([, tool]: [string, RegisteredTool]) => tool.enabled)
.map(([name, tool]: [string, RegisteredTool]) => {
const inputSchema = tool.inputSchema
? (zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as JsonSchemaObject)
: EMPTY_OBJECT_JSON_SCHEMA;

const toolDefinition: Record<string, unknown> = {
name,
title: tool.title,
description: tool.description,
inputSchema:
name === "managePermissions"
? applyManagePermissionsCreateUserRequirements(inputSchema)
: inputSchema,
annotations: tool.annotations,
};

if (tool.outputSchema) {
toolDefinition.outputSchema = zodToJsonSchema(tool.outputSchema, {
strictUnions: true,
});
}

return toolDefinition;
}),
}));
}

// 默认插件列表
const DEFAULT_PLUGINS = [
"env",
Expand Down Expand Up @@ -284,6 +364,8 @@ export async function createCloudBaseMcpServer(options?: {
}
}

overrideListToolsHandler(server);

return server;
}

Expand Down
20 changes: 8 additions & 12 deletions mcp/src/tools/permissions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,18 +324,14 @@ describe("permission tools", () => {
});
const payload = JSON.parse(result.content[0].text);

expect(mockCreateUser).toHaveBeenCalledWith({
name: "bob",
password: "secret123",
userStatus: undefined,
description: undefined,
type: undefined,
nickName: undefined,
phone: undefined,
email: undefined,
avatarUrl: undefined,
uid: undefined,
});
expect(mockCreateUser).toHaveBeenCalledWith(
expect.objectContaining({
name: "bob",
password: "secret123",
userStatus: undefined,
description: undefined,
}),
);
expect(payload).toMatchObject({
success: true,
data: {
Expand Down
27 changes: 15 additions & 12 deletions mcp/src/tools/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ type QueryPermissionAction = (typeof QUERY_PERMISSION_ACTIONS)[number];
type ManagePermissionAction = (typeof MANAGE_PERMISSION_ACTIONS)[number];
type LegacyResourceType = "noSqlDatabase" | "sqlDatabase" | "function" | "storage";

const SECURITY_RULE_DESCRIPTION =
"资源类型特定的规则内容,详细语义依赖 `resourceType`。当 `resourceType=\"noSqlDatabase\"` 且 `permission=\"CUSTOM\"` 时,应传文档数据库安全规则 JSON(文档型数据库规则:`https://docs.cloudbase.net/database/security-rules`);键通常为 `read` / `create` / `update` / `delete`,值为表达式。" +
"重要:`create` 规则验证写入数据,此时文档尚不存在,不能使用 `doc.*`;`read` / `update` / `delete` 规则可使用 `doc.*` 引用已有文档字段。" +
"不要把 `doc._openid`、`auth.openid`、查询条件子集校验或 `create` / `update` / `delete` 模板误用于 `function`、`storage` 或 `sqlDatabase`。" +
'如需配置 `function` 或 `storage`,请改查官方安全规则文档:云函数 `https://docs.cloudbase.net/cloud-function/security-rules`,云存储 `https://docs.cloudbase.net/storage/security-rules`。示例:{"read":"auth.uid != null","create":"auth.uid != null && auth.loginType != "ANONYMOUS"","update":"auth.uid != null && doc._openid == auth.openid","delete":"auth.uid != null && doc._openid == auth.openid"}';

const USERNAME_DESCRIPTION =
'用户名。**`action="createUser"` 时必填**,需与 `password` 一起提供。';
const PASSWORD_DESCRIPTION =
'密码。**`action="createUser"` 时必填**,缺失会导致创建用户失败;如果用户未指定密码,请先和用户确认或生成一个安全的随机密码再调用工具,不要遗漏该参数。`action="updateUser"` 时可选,仅在需要重置密码时提供。';

type ToolEnvelope = {
success: boolean;
data: Record<string, unknown>;
Expand Down Expand Up @@ -563,7 +574,7 @@ export function registerPermissionTools(server: ExtendedMcpServer) {
{
title: "管理权限与用户配置",
description:
"权限域统一写入口。支持修改资源权限、角色管理、成员与策略增删、应用用户 CRUD。`createUser` / `updateUser` 是环境侧应用用户管理能力,适合测试账号、管理员或预置用户,不应替代浏览器里的 Web SDK 注册表单;前端用户名密码注册应使用 `auth.signUp({ username, password })`,登录应使用 `auth.signInWithPassword({ username, password })`。注意:`securityRule` 的详细语义取决于 `resourceType`;`doc._openid`、`auth.openid`、查询条件子集校验,以及 `create` / `update` / `delete` JSON 模板仅适用于 `resourceType=\"noSqlDatabase\"` 的文档数据库安全规则。配置 `function` 或 `storage` 时,请参考各自官方安全规则文档,而不是复用 NoSQL 模板。",
"权限域统一写入口。支持修改资源权限、角色管理、成员与策略增删、应用用户 CRUD。`createUser` / `updateUser` 是环境侧应用用户管理能力,适合测试账号、管理员或预置用户,不应替代浏览器里的 Web SDK 注册表单;前端用户名密码注册应使用 `auth.signUp({ username, password })`,登录应使用 `auth.signInWithPassword({ username, password })`。**必填参数提示:`action=\"createUser\"` 时必须同时提供 `username` 和 `password` 两个参数;如果用户没有显式给出密码,请在调用工具之前先与用户确认密码或自行生成一个安全的随机密码,不要省略 `password` 字段。** 注意:`securityRule` 的详细语义取决于 `resourceType`;`doc._openid`、`auth.openid`、查询条件子集校验,以及 `create` / `update` / `delete` JSON 模板仅适用于 `resourceType=\"noSqlDatabase\"` 的文档数据库安全规则。配置 `function` 或 `storage` 时,请参考各自官方安全规则文档,而不是复用 NoSQL 模板。",
inputSchema: {
action: z.enum(MANAGE_PERMISSION_ACTIONS),
resourceType: z
Expand All @@ -574,15 +585,7 @@ export function registerPermissionTools(server: ExtendedMcpServer) {
permission: z
.enum(["READONLY", "PRIVATE", "ADMINWRITE", "ADMINONLY", "CUSTOM"])
.optional(),
securityRule: z
.string()
.optional()
.describe(
"资源类型特定的规则内容,详细语义依赖 `resourceType`。当 `resourceType=\"noSqlDatabase\"` 且 `permission=\"CUSTOM\"` 时,应传文档数据库安全规则 JSON(文档型数据库规则:`https://docs.cloudbase.net/database/security-rules`);键通常为 `read` / `create` / `update` / `delete`,值为表达式。" +
"重要:`create` 规则验证写入数据,此时文档尚不存在,不能使用 `doc.*`;`read` / `update` / `delete` 规则可使用 `doc.*` 引用已有文档字段。" +
"不要把 `doc._openid`、`auth.openid`、查询条件子集校验或 `create` / `update` / `delete` 模板误用于 `function`、`storage` 或 `sqlDatabase`。" +
'如需配置 `function` 或 `storage`,请改查官方安全规则文档:云函数 `https://docs.cloudbase.net/cloud-function/security-rules`,云存储 `https://docs.cloudbase.net/storage/security-rules`。示例:{"read":"auth.uid != null","create":"auth.uid != null && auth.loginType != "ANONYMOUS"","update":"auth.uid != null && doc._openid == auth.openid","delete":"auth.uid != null && doc._openid == auth.openid"}',
),
securityRule: z.string().optional().describe(SECURITY_RULE_DESCRIPTION),
roleId: z.string().optional(),
roleIds: z.array(z.string()).optional(),
roleName: z.string().optional(),
Expand All @@ -592,8 +595,8 @@ export function registerPermissionTools(server: ExtendedMcpServer) {
policies: z.array(z.record(z.any())).optional(),
uid: z.string().optional(),
uids: z.array(z.string()).optional(),
username: z.string().optional(),
password: z.string().optional(),
username: z.string().optional().describe(USERNAME_DESCRIPTION),
password: z.string().optional().describe(PASSWORD_DESCRIPTION),
userStatus: z.enum(["ACTIVE", "BLOCKED"]).optional(),
},
annotations: {
Expand Down
Loading