diff --git a/packages/service/core/app/mcp.ts b/packages/service/core/app/mcp.ts index 1c8ac736ea..24d3e4ffeb 100644 --- a/packages/service/core/app/mcp.ts +++ b/packages/service/core/app/mcp.ts @@ -10,9 +10,16 @@ import type { McpToolDataType } from '@fastgpt/global/core/app/tool/mcpTool/type import { UserError } from '@fastgpt/global/common/error/utils'; import $RefParser from '@apidevtools/json-schema-ref-parser'; import { getLogger, LogCategories } from '../../common/logger'; +import { isInternalAddress, PRIVATE_URL_TEXT } from '../../common/system/utils'; const logger = getLogger(LogCategories.MODULE.APP.MCP_TOOLS); +export const assertMCPUrlNotInternal = async (url: string) => { + if (await isInternalAddress(url)) { + return Promise.reject(PRIVATE_URL_TEXT); + } +}; + export class MCPClient { private client: Client; private url: string; diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts index b82e8ab55e..e966800697 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts @@ -16,7 +16,7 @@ import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { pushTrack } from '../../../../../../../common/middle/tracks/utils'; import { getErrText } from '@fastgpt/global/common/error/utils'; import { getAppVersionById } from '../../../../../../app/version/controller'; -import { MCPClient } from '../../../../../../app/mcp'; +import { assertMCPUrlNotInternal, MCPClient } from '../../../../../../app/mcp'; import { runHTTPTool } from '../../../../../../app/http'; import { getS3ChatSource } from '../../../../../../../common/s3/sources/chat'; import { parseToolId } from '../../../../child/runTool'; @@ -197,6 +197,9 @@ export const dispatchTool = async ({ const { headerSecret, url } = tool.nodes[0].toolConfig?.mcpToolSet ?? tool.nodes[0].inputs[0].value; + + await assertMCPUrlNotInternal(url); + const mcpClient = new MCPClient({ url, headers: getSecretValue({ diff --git a/packages/service/core/workflow/dispatch/child/runTool.ts b/packages/service/core/workflow/dispatch/child/runTool.ts index 53dcf0db92..dea584c1f3 100644 --- a/packages/service/core/workflow/dispatch/child/runTool.ts +++ b/packages/service/core/workflow/dispatch/child/runTool.ts @@ -7,7 +7,7 @@ import { type ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/type'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; -import { MCPClient } from '../../../app/mcp'; +import { assertMCPUrlNotInternal, MCPClient } from '../../../app/mcp'; import { getSecretValue } from '../../../../common/secret/utils'; import type { McpToolDataType } from '@fastgpt/global/core/app/tool/mcpTool/type'; import type { HttpToolConfigType } from '@fastgpt/global/core/app/tool/httpTool/type'; @@ -214,6 +214,8 @@ export const dispatchRunTool = async (props: RunToolProps): Promise ({ } })); -import { MCPClient, getMCPChildren } from '@fastgpt/service/core/app/mcp'; +import { MCPClient, assertMCPUrlNotInternal, getMCPChildren } from '@fastgpt/service/core/app/mcp'; import type { AppSchemaType } from '@fastgpt/global/core/app/type'; // Access private client via prototype for spying @@ -38,6 +38,18 @@ beforeEach(() => { describe('MCPClient', () => { const config = { url: 'http://localhost:3000/mcp', headers: { Authorization: 'Bearer test' } }; + describe('assertMCPUrlNotInternal', () => { + it('should reject localhost MCP endpoints', async () => { + await expect(assertMCPUrlNotInternal('http://localhost:3000/mcp')).rejects.toBe( + 'Request to private network not allowed' + ); + }); + + it('should allow public MCP endpoints', async () => { + await expect(assertMCPUrlNotInternal('https://example.com/mcp')).resolves.toBeUndefined(); + }); + }); + // Helper: stub getConnection to avoid real network calls const stubConnection = (mcpClient: MCPClient) => { const client = getPrivateClient(mcpClient); diff --git a/projects/app/src/pages/api/core/app/mcpTools/create.ts b/projects/app/src/pages/api/core/app/mcpTools/create.ts index e0b07e38d2..4182e2ea6d 100644 --- a/projects/app/src/pages/api/core/app/mcpTools/create.ts +++ b/projects/app/src/pages/api/core/app/mcpTools/create.ts @@ -17,6 +17,7 @@ import { type CreateMcpToolsBodyType, type CreateMcpToolsResponseType } from '@fastgpt/global/openapi/core/app/mcpTools/api'; +import { assertMCPUrlNotInternal } from '@fastgpt/service/core/app/mcp'; export type createMCPToolsQuery = {}; @@ -37,6 +38,8 @@ async function handler( ? await authApp({ req, appId: parentId, per: WritePermissionVal, authToken: true }) : await authUserPer({ req, authToken: true, per: TeamAppCreatePermissionVal }); + await assertMCPUrlNotInternal(url); + await checkTeamAppTypeLimit({ teamId, appCheckType: 'tool' }); const formatedHeaderAuth = storeSecretValue(headerSecret); diff --git a/projects/app/src/pages/api/core/app/mcpTools/getTools.ts b/projects/app/src/pages/api/core/app/mcpTools/getTools.ts index 2b43195e34..cdb6a8bcf8 100644 --- a/projects/app/src/pages/api/core/app/mcpTools/getTools.ts +++ b/projects/app/src/pages/api/core/app/mcpTools/getTools.ts @@ -1,6 +1,6 @@ import { NextAPI } from '@/service/middleware/entry'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; -import { MCPClient } from '@fastgpt/service/core/app/mcp'; +import { assertMCPUrlNotInternal, MCPClient } from '@fastgpt/service/core/app/mcp'; import { getSecretValue } from '@fastgpt/service/common/secret/utils'; import { GetMcpToolsBodySchema, @@ -8,7 +8,6 @@ import { type GetMcpToolsBodyType, type GetMcpToolsResponseType } from '@fastgpt/global/openapi/core/app/mcpTools/api'; -import { isInternalAddress, PRIVATE_URL_TEXT } from '@fastgpt/service/common/system/utils'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; async function handler( @@ -19,9 +18,7 @@ async function handler( const { url, headerSecret } = GetMcpToolsBodySchema.parse(req.body); - if (await isInternalAddress(url)) { - return Promise.reject(PRIVATE_URL_TEXT); - } + await assertMCPUrlNotInternal(url); const mcpClient = new MCPClient({ url, diff --git a/projects/app/src/pages/api/core/app/mcpTools/runTool.ts b/projects/app/src/pages/api/core/app/mcpTools/runTool.ts index fe8e13b4ed..654626f193 100644 --- a/projects/app/src/pages/api/core/app/mcpTools/runTool.ts +++ b/projects/app/src/pages/api/core/app/mcpTools/runTool.ts @@ -1,13 +1,12 @@ import { NextAPI } from '@/service/middleware/entry'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; -import { MCPClient } from '@fastgpt/service/core/app/mcp'; +import { assertMCPUrlNotInternal, MCPClient } from '@fastgpt/service/core/app/mcp'; import { getSecretValue } from '@fastgpt/service/common/secret/utils'; import { RunMcpToolBodySchema, type RunMcpToolBodyType, type RunMcpToolResponseType } from '@fastgpt/global/openapi/core/app/mcpTools/api'; -import { isInternalAddress, PRIVATE_URL_TEXT } from '@fastgpt/service/common/system/utils'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; async function handler( @@ -18,9 +17,7 @@ async function handler( const { url, toolName, headerSecret, params } = RunMcpToolBodySchema.parse(req.body); - if (await isInternalAddress(url)) { - return Promise.reject(PRIVATE_URL_TEXT); - } + await assertMCPUrlNotInternal(url); const mcpClient = new MCPClient({ url, diff --git a/projects/app/src/pages/api/core/app/mcpTools/update.ts b/projects/app/src/pages/api/core/app/mcpTools/update.ts index 87617d73ce..f2ff32a46b 100644 --- a/projects/app/src/pages/api/core/app/mcpTools/update.ts +++ b/projects/app/src/pages/api/core/app/mcpTools/update.ts @@ -13,6 +13,7 @@ import { UpdateMcpToolsBodySchema, type UpdateMcpToolsBodyType } from '@fastgpt/global/openapi/core/app/mcpTools/api'; +import { assertMCPUrlNotInternal } from '@fastgpt/service/core/app/mcp'; export type updateMCPToolsQuery = {}; @@ -20,6 +21,8 @@ async function handler(req: ApiRequestProps, res: ApiRes const { appId, url, toolList, headerSecret } = UpdateMcpToolsBodySchema.parse(req.body); const { app } = await authApp({ req, authToken: true, appId, per: ManagePermissionVal }); + await assertMCPUrlNotInternal(url); + const formatedHeaderAuth = storeSecretValue(headerSecret); // create tool set node