Commit 5a221391 by Archer Committed by GitHub

fix: clarify MCP key runtime authorization snapshot (#6992)

parent d059cbaf
import { describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { import {
pluginNodes2InputSchema, pluginNodes2InputSchema,
workflow2InputSchema, workflow2InputSchema,
...@@ -49,6 +49,10 @@ vi.mock('@fastgpt/service/core/chat/saveChat', () => ({ ...@@ -49,6 +49,10 @@ vi.mock('@fastgpt/service/core/chat/saveChat', () => ({
pushChatRecords: vi.fn() pushChatRecords: vi.fn()
})); }));
beforeEach(() => {
vi.clearAllMocks();
});
describe('pluginNodes2InputSchema', () => { describe('pluginNodes2InputSchema', () => {
it('should generate input schema from plugin nodes', () => { it('should generate input schema from plugin nodes', () => {
const nodes = [ const nodes = [
...@@ -190,6 +194,52 @@ describe('getMcpServerTools', () => { ...@@ -190,6 +194,52 @@ describe('getMcpServerTools', () => {
expect(tools[0].name).toBe('test-tool'); expect(tools[0].name).toBe('test-tool');
}); });
it('should use MCP key bindings as runtime tool snapshot', async () => {
const mockMcp = {
tmbId: 'test-tmb',
apps: [
{
appId: 'test-app',
toolName: 'test-tool',
description: 'test description'
}
]
};
vi.mocked(MongoMcpKey.findOne).mockReturnValue({
lean: () => mockMcp
});
vi.mocked(MongoApp.find).mockReturnValue({
lean: () => [
{
_id: 'test-app',
name: 'Test App',
type: AppTypeEnum.workflowTool
}
]
});
vi.mocked(authAppByTmbId).mockRejectedValue(new Error('unAuthApp'));
vi.mocked(getAppLatestVersion).mockResolvedValue({
nodes: [
{
flowNodeType: FlowNodeTypeEnum.pluginInput,
inputs: []
}
],
edges: [],
chatConfig: {}
});
const tools = await getMcpServerTools('test-key');
expect(authAppByTmbId).not.toHaveBeenCalled();
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('test-tool');
});
it('should reject if key not found', async () => { it('should reject if key not found', async () => {
vi.mocked(MongoMcpKey.findOne).mockReturnValue({ vi.mocked(MongoMcpKey.findOne).mockReturnValue({
lean: () => null lean: () => null
......
import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema'; import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { authAppByTmbId } from '@fastgpt/service/support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
import { type Tool } from '@modelcontextprotocol/sdk/types.js'; import { type Tool } from '@modelcontextprotocol/sdk/types.js';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
...@@ -113,6 +111,12 @@ export const workflow2InputSchema = (chatConfig?: { ...@@ -113,6 +111,12 @@ export const workflow2InputSchema = (chatConfig?: {
return schema; return schema;
}; };
/**
* 获取 MCP key 当前绑定的工具列表。
*
* MCP key 在创建或更新绑定应用时已经完成权限校验;运行时按 key 中保存的应用快照提供工具,
* 不再因为创建人的应用权限后续变化而隐藏工具,避免已发布集成被普通权限调整意外中断。
*/
export const getMcpServerTools = async (key: string): Promise<Tool[]> => { export const getMcpServerTools = async (key: string): Promise<Tool[]> => {
const mcp = await MongoMcpKey.findOne({ key }, { apps: 1 }).lean(); const mcp = await MongoMcpKey.findOne({ key }, { apps: 1 }).lean();
if (!mcp) { if (!mcp) {
...@@ -130,26 +134,12 @@ export const getMcpServerTools = async (key: string): Promise<Tool[]> => { ...@@ -130,26 +134,12 @@ export const getMcpServerTools = async (key: string): Promise<Tool[]> => {
{ name: 1, intro: 1 } { name: 1, intro: 1 }
).lean(); ).lean();
// Filter not permission app
const permissionAppList = await Promise.all(
appList.filter(async (app) => {
try {
await authAppByTmbId({ tmbId: mcp.tmbId, appId: app._id, per: ReadPermissionVal });
return true;
} catch (error) {
return false;
}
})
);
// Get latest version // Get latest version
const versionList = await Promise.all( const versionList = await Promise.all(appList.map((app) => getAppLatestVersion(app._id, app)));
permissionAppList.map((app) => getAppLatestVersion(app._id, app))
);
// Compute mcp tools // Compute mcp tools
const tools = versionList.map<Tool>((version, index) => { const tools = versionList.map<Tool>((version, index) => {
const app = permissionAppList[index]; const app = appList[index];
const mcpApp = mcp.apps.find((mcpApp) => String(mcpApp.appId) === String(app._id))!; const mcpApp = mcp.apps.find((mcpApp) => String(mcpApp.appId) === String(app._id))!;
const isPlugin = !!version.nodes.find( const isPlugin = !!version.nodes.find(
...@@ -168,7 +158,12 @@ export const getMcpServerTools = async (key: string): Promise<Tool[]> => { ...@@ -168,7 +158,12 @@ export const getMcpServerTools = async (key: string): Promise<Tool[]> => {
return tools; return tools;
}; };
// Call tool /**
* 调用 MCP key 已绑定的工具。
*
* 这里延续 MCP key 的绑定快照语义:调用时只校验 key 和 toolName 是否存在于绑定关系中,
* 不根据创建人的实时应用权限再次拒绝执行;如需撤销 MCP 访问,应更新或删除对应 MCP key。
*/
export const callMcpServerTool = async ({ key, toolName, inputs }: toolCallProps) => { export const callMcpServerTool = async ({ key, toolName, inputs }: toolCallProps) => {
const dispatchApp = async (app: AppSchemaType, variables: Record<string, any>) => { const dispatchApp = async (app: AppSchemaType, variables: Record<string, any>) => {
const isPlugin = app.type === AppTypeEnum.workflowTool; const isPlugin = app.type === AppTypeEnum.workflowTool;
...@@ -215,34 +210,28 @@ export const callMcpServerTool = async ({ key, toolName, inputs }: toolCallProps ...@@ -215,34 +210,28 @@ export const callMcpServerTool = async ({ key, toolName, inputs }: toolCallProps
const chatId = getNanoid(); const chatId = getNanoid();
const { const { assistantResponses, newVariables, flowResponses, durationSeconds, system_memories } =
flowUsages, await dispatchWorkFlow({
assistantResponses, chatId,
newVariables, mode: 'chat',
flowResponses, usageSource: UsageSourceEnum.mcp,
durationSeconds, runningAppInfo: {
system_memories id: String(app._id),
} = await dispatchWorkFlow({ name: app.name,
chatId, teamId: String(app.teamId),
mode: 'chat', tmbId: String(app.tmbId)
usageSource: UsageSourceEnum.mcp, },
runningAppInfo: { runningUserInfo: await getRunningUserInfoByTmbId(app.tmbId),
id: String(app._id), uid: String(app.tmbId),
name: app.name, runtimeNodes,
teamId: String(app.teamId), runtimeEdges: storeEdges2RuntimeEdges(edges),
tmbId: String(app.tmbId) variables,
}, query: removeEmptyUserInput(userQuestion.value),
runningUserInfo: await getRunningUserInfoByTmbId(app.tmbId), chatConfig,
uid: String(app.tmbId), histories: [],
runtimeNodes, stream: false,
runtimeEdges: storeEdges2RuntimeEdges(edges), maxRunTimes: WORKFLOW_MAX_RUN_TIMES
variables, });
query: removeEmptyUserInput(userQuestion.value),
chatConfig,
histories: [],
stream: false,
maxRunTimes: WORKFLOW_MAX_RUN_TIMES
});
// Save chat // Save chat
const aiResponse: AIChatItemType & { dataId?: string } = { const aiResponse: AIChatItemType & { dataId?: string } = {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment