├── .cursor └── environment.json ├── .cursorrules ├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── #0_bug_report_zh.yml │ ├── #1_feature_request_zh.yml │ ├── #2_question_zh.yml │ ├── #3_proposal_zh.yml │ ├── #4_discussion_zh.yml │ ├── 0_bug_report.yml │ ├── 1_feature_request.yml │ ├── 2_question.yml │ ├── 3_proposal.yml │ └── 4_discussion.yml └── workflows │ ├── codeql.yml │ ├── docker-publish-allinone.yml │ ├── docker-publish-base.yml │ ├── docker-publish-mcp-gateway.yml │ ├── docker-publish-mock-server.yml │ ├── docker-publish-web.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── changelog ├── v0.2.10.md ├── v0.2.11.md ├── v0.2.2.md ├── v0.2.4.md ├── v0.2.5.md ├── v0.2.6.md ├── v0.2.7.md ├── v0.2.8.md ├── v0.2.9.md ├── v0.3.0.md ├── v0.3.1.md ├── v0.3.2.md ├── v0.3.3.md ├── v0.3.4.md ├── v0.3.5.md ├── v0.4.0.md ├── v0.4.1.md ├── v0.4.2.md ├── v0.4.3.md ├── v0.4.4.md ├── v0.4.5.md ├── v0.4.6.md ├── v0.4.7.md ├── v0.5.0.md └── v0.5.1.md ├── cmd ├── apiserver │ └── main.go ├── mcp-gateway │ └── main.go ├── mock-server │ ├── backend │ │ ├── http-server.go │ │ ├── image │ │ └── mcp-server.go │ └── main.go └── openapi-converter │ ├── examples │ ├── mock-user-svc.v3.yaml │ └── petstore.v3.yaml │ └── main.go ├── configs ├── apiserver.yaml ├── i18n │ ├── en.toml │ └── zh.toml ├── mcp-gateway.yaml ├── proxy-mcp-exp.yaml └── proxy-mock-server.yaml ├── deploy ├── docker │ ├── allinone │ │ ├── Dockerfile │ │ ├── docker-compose.yml │ │ ├── nginx.conf │ │ └── supervisord.conf │ ├── base │ │ └── Dockerfile │ └── multi │ │ ├── docker-compose.yml │ │ ├── mcp-gateway │ │ └── Dockerfile │ │ ├── mock-server │ │ └── Dockerfile │ │ ├── nginx.conf │ │ └── web │ │ ├── Dockerfile │ │ └── nginx.conf └── k8s │ └── multi │ ├── app-configs.yaml │ ├── app-env.yaml │ ├── env │ ├── .env.example │ └── proxy-mock-server.yaml │ ├── i18n-config.yaml │ ├── mcp-gateway-deployment.yaml │ ├── mock-user-deployment.yaml │ ├── postgres-deployment.yaml │ └── web-deployment.yaml ├── docs ├── .ai │ ├── SOP.client.zh-CN.md │ ├── SOP.server.zh-CN.md │ └── release.md ├── README.zh-CN.md ├── i18n.md ├── i18n_direct_errors.md ├── i18n_example.md ├── i18n_summary.md ├── requirements │ └── mvp.server.md └── specs │ ├── MCP-Error-Handling.md │ └── all-in-one.yaml ├── go.mod ├── go.sum ├── internal ├── apiserver │ ├── database │ │ ├── database.go │ │ ├── factory.go │ │ ├── model.go │ │ ├── mysql.go │ │ ├── postgres.go │ │ ├── sqlite.go │ │ ├── tx.go │ │ └── util.go │ ├── handler │ │ ├── auth.go │ │ ├── chat.go │ │ ├── mcp.go │ │ ├── openapi.go │ │ ├── tenant.go │ │ └── websocket.go │ └── middleware │ │ └── jwt.go ├── auth │ ├── authenticator.go │ ├── impl │ │ ├── apikey.go │ │ ├── basic.go │ │ ├── bearer.go │ │ ├── noop.go │ │ ├── oauth2.go │ │ └── oauth2_test.go │ ├── jwt │ │ └── jwt.go │ ├── oauth2 │ │ └── client.go │ └── types │ │ ├── authenticator.go │ │ └── types.go ├── common │ ├── cnst │ │ ├── action.go │ │ ├── app.go │ │ ├── config.go │ │ ├── errors.go │ │ ├── i18n.go │ │ ├── policy.go │ │ └── proto.go │ ├── config │ │ ├── apiserver.go │ │ ├── config.go │ │ ├── mcp.go │ │ ├── notifier.go │ │ ├── storage.go │ │ └── validator.go │ └── dto │ │ ├── auth.go │ │ ├── mcp.go │ │ └── websocket.go ├── core │ ├── handler.go │ ├── mcpproxy │ │ ├── common.go │ │ ├── sse.go │ │ ├── stdio.go │ │ ├── streamable.go │ │ └── transport.go │ ├── middleware.go │ ├── response.go │ ├── server.go │ ├── sse.go │ ├── state │ │ ├── getter.go │ │ └── state.go │ ├── storage.go │ ├── streamable.go │ └── tool.go ├── i18n │ ├── const.go │ ├── core.go │ ├── error.go │ ├── errorresponse.go │ └── response.go ├── mcp │ ├── session │ │ ├── factory.go │ │ ├── memory.go │ │ ├── redis.go │ │ └── session.go │ └── storage │ │ ├── api.go │ │ ├── db.go │ │ ├── disk.go │ │ ├── factory.go │ │ ├── model.go │ │ ├── notifier │ │ ├── api.go │ │ ├── composite.go │ │ ├── factory.go │ │ ├── notifier.go │ │ ├── redis.go │ │ └── signal.go │ │ └── store.go └── template │ ├── context.go │ ├── funcs.go │ └── renderer.go ├── pkg ├── helper │ ├── config.go │ └── pid.go ├── logger │ └── logger.go ├── mcp │ ├── cnst.go │ └── server_types.go ├── openai │ └── client.go ├── openapi │ ├── converter.go │ └── converter_test.go ├── utils │ ├── env.go │ ├── env_test.go │ ├── pid.go │ ├── pid_test.go │ ├── process.go │ ├── process_test.go │ └── str.go └── version │ ├── VERSION │ └── version.go └── web ├── .env.example ├── .npmrc ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── logo.png └── wechat-qrcode.png ├── src ├── App.tsx ├── components │ ├── AccessibleModal.tsx │ ├── ChangePasswordDialog.tsx │ ├── LanguageSwitcher.tsx │ ├── Layout.tsx │ ├── WechatQRCode.tsx │ └── ui │ │ └── MultiSelectAutocomplete.tsx ├── env.d.ts ├── hooks │ └── useTheme.ts ├── i18n │ ├── index.ts │ └── locales │ │ ├── en │ │ └── translation.json │ │ └── zh │ │ └── translation.json ├── index.css ├── main.tsx ├── pages │ ├── auth │ │ └── login.tsx │ ├── chat │ │ ├── chat-context.tsx │ │ ├── chat-interface.tsx │ │ └── components │ │ │ ├── chat-history.tsx │ │ │ └── chat-message.tsx │ ├── gateway │ │ ├── components │ │ │ ├── ConfigEditor.tsx │ │ │ ├── MCPServersConfig.tsx │ │ │ ├── OpenAPIImport.tsx │ │ │ ├── RouterConfig.tsx │ │ │ ├── ServersConfig.tsx │ │ │ └── ToolsConfig.tsx │ │ ├── config-versions.tsx │ │ ├── constants │ │ │ └── defaultConfig.ts │ │ └── gateway-manager.tsx │ └── users │ │ ├── tenant-management.tsx │ │ └── user-management.tsx ├── services │ ├── api.ts │ ├── mcp.ts │ └── websocket.ts ├── types │ ├── gateway.ts │ ├── mcp.ts │ ├── message.ts │ ├── monaco.d.ts │ └── user.ts └── utils │ ├── error-handler.ts │ ├── i18n-utils.ts │ ├── toast.ts │ └── utils.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.cursor/environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "snapshot": "snapshot-20250526-4677a5f5-e9d5-455e-924e-a7df1af56316", 3 | "install": "go mod download && cd web && npm i", 4 | "terminals": [] 5 | } -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | - All comments must be in English and only included where the code is not self-explanatory—avoid redundant or obvious comments. 2 | - You can read docs/.ai/SOP.server.zh-CN.md for server when the task starts. 3 | - You can read docs/.ai/SOP.client.zh-CN.md for web when the task starts. 4 | - Please check out docs/.ai/release.md for the release process. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/#0_bug_report_zh.yml: -------------------------------------------------------------------------------- 1 | name: "🐞 Bug 报告" 2 | description: "提交 Bug 帮助我们改进" 3 | title: "[BUG] " 4 | labels: ["bug"] 5 | body: 6 | - type: checkboxes 7 | id: duplicate-check 8 | attributes: 9 | label: "⚠️ 验证" 10 | description: "请确认您已经完成以下操作:" 11 | options: 12 | - label: 我已经搜索过 [issues](https://github.com/mcp-ecosystem/mcp-gateway/issues),确信这不是一个重复的问题。 13 | required: true 14 | 15 | - type: markdown 16 | attributes: 17 | value: "## 🔍 环境信息" 18 | 19 | - type: input 20 | id: go-version 21 | attributes: 22 | label: "Go 版本" 23 | description: "您使用的 Go 版本" 24 | placeholder: "1.21.0" 25 | validations: 26 | required: true 27 | 28 | - type: input 29 | id: gateway-version 30 | attributes: 31 | label: "mcp-gateway 版本" 32 | description: "您使用的 mcp-gateway 版本" 33 | placeholder: "v1.0.0" 34 | validations: 35 | required: true 36 | 37 | - type: dropdown 38 | id: platform 39 | attributes: 40 | label: Platform 41 | description: What platform are you using? 42 | options: 43 | - Windows 44 | - macOS 45 | - Linux 46 | validations: 47 | required: true 48 | 49 | - type: textarea 50 | id: bug-description 51 | attributes: 52 | label: "📝 Bug 描述" 53 | description: "清晰简洁地描述这个 bug。" 54 | placeholder: "请告诉我们您遇到了什么问题。" 55 | validations: 56 | required: true 57 | 58 | - type: textarea 59 | id: reproduction-steps 60 | attributes: 61 | label: "🔄 复现步骤" 62 | description: "如何复现这个问题?" 63 | placeholder: | 64 | 1. 第一步 65 | 2. 第二步 66 | 3. 第三步 67 | 4. ... 68 | 69 | 如果可能,请提供 GitHub 仓库链接以复现此问题。 70 | validations: 71 | required: true 72 | 73 | - type: textarea 74 | id: expected-behavior 75 | attributes: 76 | label: "✅ 预期行为" 77 | description: "您期望发生什么?" 78 | placeholder: "描述您期望发生的情况" 79 | validations: 80 | required: true 81 | 82 | - type: textarea 83 | id: actual-behavior 84 | attributes: 85 | label: "❌ 实际行为" 86 | description: "实际发生了什么?" 87 | placeholder: "描述实际发生的情况" 88 | validations: 89 | required: true 90 | 91 | - type: textarea 92 | id: possible-solution 93 | attributes: 94 | label: "💡 可能的解决方案" 95 | description: "如果您对如何解决这个问题有想法,请在此分享。" 96 | placeholder: "您对解决此问题的建议" 97 | validations: 98 | required: false 99 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/#1_feature_request_zh.yml: -------------------------------------------------------------------------------- 1 | name: "✨ 功能请求" 2 | description: "提出一个新想法或建议" 3 | title: "[功能] " 4 | labels: ["enhancement"] 5 | body: 6 | - type: checkboxes 7 | id: verification 8 | attributes: 9 | label: "⚠️ 验证" 10 | description: "请确认您已经完成以下操作:" 11 | options: 12 | - label: 我已经搜索过 [issues](https://github.com/mcp-ecosystem/mcp-gateway/issues),确信这不是一个重复的请求。 13 | required: true 14 | - label: 我已经查看了 [发布说明](https://github.com/mcp-ecosystem/mcp-gateway/releases),确信这项功能尚未被实现。 15 | required: true 16 | 17 | - type: textarea 18 | id: solution-description 19 | attributes: 20 | label: "🎯 解决方案描述" 21 | description: "对提议的方法或功能的清晰概述。" 22 | placeholder: "描述您希望看到的解决方案" 23 | validations: 24 | required: true 25 | 26 | - type: textarea 27 | id: use-cases 28 | attributes: 29 | label: "📋 使用场景" 30 | description: "这个解决方案适用的典型场景。" 31 | placeholder: "描述这个功能在哪些情况下会有用" 32 | validations: 33 | required: true 34 | 35 | - type: textarea 36 | id: complexity-risks 37 | attributes: 38 | label: "⚖️ 复杂性与风险" 39 | description: "潜在的挑战、技术障碍或缺点。" 40 | placeholder: "描述可能存在的任何挑战或顾虑" 41 | validations: 42 | required: false 43 | 44 | - type: textarea 45 | id: external-dependencies 46 | attributes: 47 | label: "🔗 外部依赖" 48 | description: "所需的第三方工具、服务或集成。" 49 | placeholder: "列出所需的任何外部工具或服务" 50 | validations: 51 | required: false 52 | 53 | - type: textarea 54 | id: additional-context 55 | attributes: 56 | label: "📘 附加上下文" 57 | description: "添加关于功能请求的任何其他上下文或截图。" 58 | placeholder: "在此添加任何其他相关信息" 59 | validations: 60 | required: false 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/#2_question_zh.yml: -------------------------------------------------------------------------------- 1 | name: "❓ 问题" 2 | description: "提出关于项目的问题" 3 | title: "[问题] " 4 | labels: ["question"] 5 | body: 6 | - type: checkboxes 7 | id: verification 8 | attributes: 9 | label: "⚠️ 验证" 10 | description: "请确认您已经完成以下操作:" 11 | options: 12 | - label: 我已经搜索过 [issues](https://github.com/mcp-ecosystem/mcp-gateway/issues),确信这不是一个重复的问题。 13 | required: true 14 | 15 | - type: textarea 16 | id: question 17 | attributes: 18 | label: "❓ 您的问题" 19 | description: "您想知道什么?" 20 | placeholder: "详细描述您的问题" 21 | validations: 22 | required: true 23 | 24 | - type: textarea 25 | id: context 26 | attributes: 27 | label: "📚 上下文" 28 | description: "添加任何可能帮助我们回答您问题的上下文" 29 | placeholder: "解释导致这个问题的任何背景或上下文" 30 | validations: 31 | required: false 32 | 33 | - type: textarea 34 | id: related-resources 35 | attributes: 36 | label: "🔗 相关资源" 37 | description: "链接到任何相关文档、代码或资源" 38 | placeholder: "分享任何相关链接或资源" 39 | validations: 40 | required: false 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/#3_proposal_zh.yml: -------------------------------------------------------------------------------- 1 | name: "📝 提案" 2 | description: "创建一个技术提案" 3 | title: "[提案] " 4 | labels: ["proposal"] 5 | body: 6 | - type: checkboxes 7 | id: verification 8 | attributes: 9 | label: "⚠️ 验证" 10 | description: "请确认您已经完成以下操作:" 11 | options: 12 | - label: 我已经搜索过 [issues](https://github.com/mcp-ecosystem/mcp-gateway/issues),确信这不是一个重复的提案。 13 | required: true 14 | 15 | - type: markdown 16 | attributes: 17 | value: | 18 | ## 📋 提案详情 19 | 请使用此模板提交具体的功能设计提案。 20 | 如果您只想请求新功能并讨论可能的业务价值,请创建功能请求。 21 | 22 | - type: textarea 23 | id: proposal-summary 24 | attributes: 25 | label: "✨ 提案摘要" 26 | description: "您提案的简要概述" 27 | placeholder: "提供您的技术提案的简明摘要" 28 | validations: 29 | required: true 30 | 31 | - type: textarea 32 | id: implementation-approach 33 | attributes: 34 | label: "🛠️ 实现方法" 35 | description: "应该如何实现这个提案?" 36 | placeholder: "描述实现此提案的方法" 37 | validations: 38 | required: true 39 | 40 | - type: textarea 41 | id: additional-context 42 | attributes: 43 | label: "📚 附加上下文" 44 | description: "任何其他相关信息" 45 | placeholder: "提供可能有助于理解您提案的任何其他上下文" 46 | validations: 47 | required: false 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/#4_discussion_zh.yml: -------------------------------------------------------------------------------- 1 | name: "💬 讨论" 2 | description: "开始一个关于项目的讨论" 3 | title: "[讨论] " 4 | labels: ["discussion"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: "## 🔄 讨论主题" 9 | 10 | - type: textarea 11 | id: discussion-content 12 | attributes: 13 | label: "讨论详情" 14 | description: "请描述您想要讨论的内容" 15 | placeholder: "提供关于您想讨论的项目相关事项的详细信息" 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | id: related-context 21 | attributes: 22 | label: "📚 相关背景" 23 | description: "添加任何相关的上下文或背景信息" 24 | placeholder: "分享有助于理解此讨论的背景信息" 25 | validations: 26 | required: false 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/0_bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "🐞 Bug Report" 2 | description: "Report a bug to help us improve" 3 | title: "[BUG] " 4 | labels: ["bug"] 5 | body: 6 | - type: checkboxes 7 | id: duplicate-check 8 | attributes: 9 | label: "⚠️ Verification" 10 | description: "Please verify that you've done the following:" 11 | options: 12 | - label: I have searched the [issues](https://github.com/mcp-ecosystem/mcp-gateway/issues) of this repository and believe that this is not a duplicate. 13 | required: true 14 | 15 | - type: markdown 16 | attributes: 17 | value: "## 🔍 Environment" 18 | 19 | - type: input 20 | id: go-version 21 | attributes: 22 | label: "Go Version" 23 | description: "The version of Go you're using" 24 | placeholder: "1.21.0" 25 | validations: 26 | required: true 27 | 28 | - type: input 29 | id: gateway-version 30 | attributes: 31 | label: "mcp-gateway Version" 32 | description: "The version of mcp-gateway you're using" 33 | placeholder: "v1.0.0" 34 | validations: 35 | required: true 36 | 37 | - type: dropdown 38 | id: platform 39 | attributes: 40 | label: Platform 41 | description: What platform are you using? 42 | options: 43 | - Windows 44 | - macOS 45 | - Linux 46 | validations: 47 | required: true 48 | 49 | - type: textarea 50 | id: bug-description 51 | attributes: 52 | label: "📝 Describe the Bug" 53 | description: "A clear and concise description of what the bug is." 54 | placeholder: "Tell us what you're seeing that doesn't work as expected." 55 | validations: 56 | required: true 57 | 58 | - type: textarea 59 | id: reproduction-steps 60 | attributes: 61 | label: "🔄 Steps to Reproduce" 62 | description: "How can we reproduce this issue?" 63 | placeholder: | 64 | 1. Step 1 65 | 2. Step 2 66 | 3. Step 3 67 | 4. ... 68 | 69 | Please provide GitHub repository link if possible to reproduce this issue. 70 | validations: 71 | required: true 72 | 73 | - type: textarea 74 | id: expected-behavior 75 | attributes: 76 | label: "✅ Expected Behavior" 77 | description: "What did you expect to happen?" 78 | placeholder: "Describe what you expected to happen" 79 | validations: 80 | required: true 81 | 82 | - type: textarea 83 | id: actual-behavior 84 | attributes: 85 | label: "❌ Actual Behavior" 86 | description: "What actually happened?" 87 | placeholder: "Describe what actually happened" 88 | validations: 89 | required: true 90 | 91 | - type: textarea 92 | id: possible-solution 93 | attributes: 94 | label: "💡 Possible Solution" 95 | description: "If you have ideas on how to fix this issue, please share them here." 96 | placeholder: "Your suggestions for fixing the problem" 97 | validations: 98 | required: false 99 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "✨ Feature Request" 2 | description: "Suggest an idea for this project" 3 | title: "[FEATURE] " 4 | labels: ["enhancement"] 5 | body: 6 | - type: checkboxes 7 | id: verification 8 | attributes: 9 | label: "⚠️ Verification" 10 | description: "Please verify that you've done the following:" 11 | options: 12 | - label: I have searched the [issues](https://github.com/mcp-ecosystem/mcp-gateway/issues) of this repository and believe that this is not a duplicate. 13 | required: true 14 | - label: I have searched the [release notes](https://github.com/mcp-ecosystem/mcp-gateway/releases) of this repository and believe that this is not a duplicate. 15 | required: true 16 | 17 | - type: textarea 18 | id: solution-description 19 | attributes: 20 | label: "🎯 Solution Description" 21 | description: "A clear overview of the proposed approach or feature." 22 | placeholder: "Describe the solution you'd like to see implemented" 23 | validations: 24 | required: true 25 | 26 | - type: textarea 27 | id: use-cases 28 | attributes: 29 | label: "📋 Use Cases" 30 | description: "Typical scenarios where this solution would be applied." 31 | placeholder: "Describe situations where this feature would be useful" 32 | validations: 33 | required: true 34 | 35 | - type: textarea 36 | id: complexity-risks 37 | attributes: 38 | label: "⚖️ Complexity & Risks" 39 | description: "Potential challenges, technical hurdles, or downsides." 40 | placeholder: "Describe any potential challenges or concerns" 41 | validations: 42 | required: false 43 | 44 | - type: textarea 45 | id: external-dependencies 46 | attributes: 47 | label: "🔗 External Dependencies" 48 | description: "Required third-party tools, services, or integrations." 49 | placeholder: "List any external tools or services needed" 50 | validations: 51 | required: false 52 | 53 | - type: textarea 54 | id: additional-context 55 | attributes: 56 | label: "📘 Additional Context" 57 | description: "Add any other context or screenshots about the feature request here." 58 | placeholder: "Add any other relevant information here" 59 | validations: 60 | required: false 61 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_question.yml: -------------------------------------------------------------------------------- 1 | name: "❓ Question" 2 | description: "Ask a question about the project" 3 | title: "[QUESTION] " 4 | labels: ["question"] 5 | body: 6 | - type: checkboxes 7 | id: verification 8 | attributes: 9 | label: "⚠️ Verification" 10 | description: "Please verify that you've done the following:" 11 | options: 12 | - label: I have searched the [issues](https://github.com/mcp-ecosystem/mcp-gateway/issues) of this repository and believe that this is not a duplicate. 13 | required: true 14 | 15 | - type: textarea 16 | id: question 17 | attributes: 18 | label: "❓ Your Question" 19 | description: "What would you like to know?" 20 | placeholder: "Ask your question in detail" 21 | validations: 22 | required: true 23 | 24 | - type: textarea 25 | id: context 26 | attributes: 27 | label: "📚 Context" 28 | description: "Add any context that might help us answer your question" 29 | placeholder: "Explain any background or context that led to this question" 30 | validations: 31 | required: false 32 | 33 | - type: textarea 34 | id: related-resources 35 | attributes: 36 | label: "🔗 Related Resources" 37 | description: "Link to any related documents, code, or resources" 38 | placeholder: "Share any related links or resources" 39 | validations: 40 | required: false 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3_proposal.yml: -------------------------------------------------------------------------------- 1 | name: "📝 Proposal" 2 | description: "Create a technical proposal" 3 | title: "[PROPOSAL] " 4 | labels: ["proposal"] 5 | body: 6 | - type: checkboxes 7 | id: verification 8 | attributes: 9 | label: "⚠️ Verification" 10 | description: "Please verify that you've done the following:" 11 | options: 12 | - label: I have searched the [issues](https://github.com/mcp-ecosystem/mcp-gateway/issues) of this repository and believe that this is not a duplicate. 13 | required: true 14 | 15 | - type: markdown 16 | attributes: 17 | value: | 18 | ## 📋 Proposal Details 19 | Please use this for a concrete design proposal for functionality. 20 | If you just want to request a new feature and discuss the possible business value, create a Feature Request instead. 21 | 22 | - type: textarea 23 | id: proposal-summary 24 | attributes: 25 | label: "✨ Proposal Summary" 26 | description: "A brief overview of your proposal" 27 | placeholder: "Provide a concise summary of your technical proposal" 28 | validations: 29 | required: true 30 | 31 | - type: textarea 32 | id: implementation-approach 33 | attributes: 34 | label: "🛠️ Implementation Approach" 35 | description: "How should this be implemented?" 36 | placeholder: "Describe the approach to implementing this proposal" 37 | validations: 38 | required: true 39 | 40 | - type: textarea 41 | id: additional-context 42 | attributes: 43 | label: "📚 Additional Context" 44 | description: "Any other relevant information" 45 | placeholder: "Provide any other context that might help understand your proposal" 46 | validations: 47 | required: false 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/4_discussion.yml: -------------------------------------------------------------------------------- 1 | name: "💬 Discussion" 2 | description: "Start a discussion about the project" 3 | title: "[DISCUSSION] " 4 | labels: ["discussion"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: "## 🔄 Discussion Topic" 9 | 10 | - type: textarea 11 | id: discussion-content 12 | attributes: 13 | label: "Discussion Details" 14 | description: "Please describe what you'd like to discuss" 15 | placeholder: "Provide details about what you want to discuss regarding the project" 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | id: related-context 21 | attributes: 22 | label: "📚 Related Context" 23 | description: "Add any relevant context or background information" 24 | placeholder: "Share any background information that helps frame this discussion" 25 | validations: 26 | required: false 27 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '30 1 * * 0' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'go' ] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | - name: Autobuild 35 | uses: github/codeql-action/autobuild@v2 36 | 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@v2 39 | with: 40 | category: "/language:${{ matrix.language }}" -------------------------------------------------------------------------------- /.github/workflows/docker-publish-base.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build and Publish Base Image 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | DOCKER_HUB_USERNAME: ifuryst 8 | ALIYUN_REGISTRY: registry.ap-southeast-1.aliyuncs.com/mcp-ecosystem 9 | BUILDX_NO_DEFAULT_ATTESTATIONS: 1 10 | 11 | jobs: 12 | build-and-push: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v3 24 | with: 25 | platforms: linux/amd64,linux/arm64 26 | 27 | - name: Login to Docker Hub 28 | uses: docker/login-action@v3 29 | with: 30 | username: ${{ env.DOCKER_HUB_USERNAME }} 31 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 32 | 33 | - name: Login to GitHub Container Registry 34 | uses: docker/login-action@v3 35 | with: 36 | registry: ghcr.io 37 | username: ${{ github.actor }} 38 | password: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | - name: Login to Aliyun Container Registry 41 | uses: docker/login-action@v3 42 | with: 43 | registry: ${{ env.ALIYUN_REGISTRY }} 44 | username: ${{ secrets.ALIYUN_USERNAME }} 45 | password: ${{ secrets.ALIYUN_PASSWORD }} 46 | 47 | - name: Build and push base image 48 | uses: docker/build-push-action@v5 49 | with: 50 | context: . 51 | file: deploy/docker/base/Dockerfile 52 | push: true 53 | platforms: linux/amd64,linux/arm64 54 | cache-from: type=gha 55 | cache-to: type=gha,mode=max 56 | build-args: | 57 | PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ 58 | UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple/ 59 | npm_config_registry=https://registry.npmmirror.com 60 | tags: | 61 | ${{ env.DOCKER_HUB_USERNAME }}/mcp-gateway-base:latest 62 | ghcr.io/${{ github.repository }}/base:latest 63 | ${{ env.ALIYUN_REGISTRY }}/mcp-gateway-base:latest -------------------------------------------------------------------------------- /.github/workflows/docker-publish-mcp-gateway.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build and Publish MCP Gateway 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | paths: 8 | - 'cmd/**' 9 | - 'internal/**' 10 | - 'pkg/**' 11 | - 'deploy/docker/**' 12 | - 'go.mod' 13 | - 'go.sum' 14 | workflow_dispatch: 15 | inputs: 16 | version: 17 | description: 'Version tag for the image' 18 | required: true 19 | default: 'dev' 20 | 21 | env: 22 | DOCKER_HUB_USERNAME: ifuryst 23 | ALIYUN_REGISTRY: registry.ap-southeast-1.aliyuncs.com/mcp-ecosystem 24 | BUILDX_NO_DEFAULT_ATTESTATIONS: 1 25 | PUSH_LATEST: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} 26 | SHOULD_LOGIN: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} 27 | 28 | jobs: 29 | build-and-push: 30 | runs-on: ubuntu-latest 31 | permissions: 32 | contents: read 33 | packages: write 34 | 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | 39 | - name: Set up Docker Buildx 40 | uses: docker/setup-buildx-action@v3 41 | with: 42 | platforms: linux/amd64,linux/arm64 43 | 44 | - name: Login to Docker Hub 45 | if: env.SHOULD_LOGIN == 'true' 46 | uses: docker/login-action@v3 47 | with: 48 | username: ${{ env.DOCKER_HUB_USERNAME }} 49 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 50 | 51 | - name: Login to GitHub Container Registry 52 | if: env.SHOULD_LOGIN == 'true' 53 | uses: docker/login-action@v3 54 | with: 55 | registry: ghcr.io 56 | username: ${{ github.actor }} 57 | password: ${{ secrets.GITHUB_TOKEN }} 58 | 59 | - name: Login to Aliyun Container Registry 60 | if: env.SHOULD_LOGIN == 'true' 61 | uses: docker/login-action@v3 62 | with: 63 | registry: ${{ env.ALIYUN_REGISTRY }} 64 | username: ${{ secrets.ALIYUN_USERNAME }} 65 | password: ${{ secrets.ALIYUN_PASSWORD }} 66 | 67 | - name: Build and push mcp-gateway image 68 | uses: docker/build-push-action@v5 69 | with: 70 | context: . 71 | file: deploy/docker/multi/mcp-gateway/Dockerfile 72 | push: true 73 | platforms: linux/amd64,linux/arm64 74 | cache-from: type=gha 75 | cache-to: type=gha,mode=max 76 | tags: | 77 | ${{ env.PUSH_LATEST == 'true' && format('{0}/mcp-gateway-mcp-gateway:latest', env.DOCKER_HUB_USERNAME) || '' }} 78 | ${{ format('{0}/mcp-gateway-mcp-gateway:{1}', env.DOCKER_HUB_USERNAME, github.ref_name) }} 79 | ${{ env.PUSH_LATEST == 'true' && format('ghcr.io/{0}/mcp-gateway:latest', github.repository) || '' }} 80 | ${{ format('ghcr.io/{0}/mcp-gateway:{1}', github.repository, github.ref_name) }} 81 | ${{ env.PUSH_LATEST == 'true' && format('{0}/mcp-gateway-mcp-gateway:latest', env.ALIYUN_REGISTRY) || '' }} 82 | ${{ format('{0}/mcp-gateway-mcp-gateway:{1}', env.ALIYUN_REGISTRY, github.ref_name) }} -------------------------------------------------------------------------------- /.github/workflows/docker-publish-mock-server.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build and Publish Mock User Service 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | paths: 8 | - 'cmd/**' 9 | - 'internal/**' 10 | - 'pkg/**' 11 | - 'deploy/docker/**' 12 | - 'go.mod' 13 | - 'go.sum' 14 | workflow_dispatch: 15 | inputs: 16 | version: 17 | description: 'Version tag for the image' 18 | required: true 19 | default: 'dev' 20 | 21 | env: 22 | DOCKER_HUB_USERNAME: ifuryst 23 | ALIYUN_REGISTRY: registry.ap-southeast-1.aliyuncs.com/mcp-ecosystem 24 | BUILDX_NO_DEFAULT_ATTESTATIONS: 1 25 | PUSH_LATEST: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} 26 | SHOULD_LOGIN: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} 27 | 28 | jobs: 29 | build-and-push: 30 | runs-on: ubuntu-latest 31 | permissions: 32 | contents: read 33 | packages: write 34 | 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | 39 | - name: Set up Docker Buildx 40 | uses: docker/setup-buildx-action@v3 41 | with: 42 | platforms: linux/amd64,linux/arm64 43 | 44 | - name: Login to Docker Hub 45 | if: env.SHOULD_LOGIN == 'true' 46 | uses: docker/login-action@v3 47 | with: 48 | username: ${{ env.DOCKER_HUB_USERNAME }} 49 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 50 | 51 | - name: Login to GitHub Container Registry 52 | if: env.SHOULD_LOGIN == 'true' 53 | uses: docker/login-action@v3 54 | with: 55 | registry: ghcr.io 56 | username: ${{ github.actor }} 57 | password: ${{ secrets.GITHUB_TOKEN }} 58 | 59 | - name: Login to Aliyun Container Registry 60 | if: env.SHOULD_LOGIN == 'true' 61 | uses: docker/login-action@v3 62 | with: 63 | registry: ${{ env.ALIYUN_REGISTRY }} 64 | username: ${{ secrets.ALIYUN_USERNAME }} 65 | password: ${{ secrets.ALIYUN_PASSWORD }} 66 | 67 | - name: Build and push mock-server image 68 | uses: docker/build-push-action@v5 69 | with: 70 | context: . 71 | file: deploy/docker/multi/mock-server/Dockerfile 72 | push: true 73 | platforms: linux/amd64,linux/arm64 74 | cache-from: type=gha 75 | cache-to: type=gha,mode=max 76 | tags: | 77 | ${{ env.PUSH_LATEST == 'true' && format('{0}/mcp-gateway-mock-server:latest', env.DOCKER_HUB_USERNAME) || '' }} 78 | ${{ format('{0}/mcp-gateway-mock-server:{1}', env.DOCKER_HUB_USERNAME, github.ref_name) }} 79 | ${{ env.PUSH_LATEST == 'true' && format('ghcr.io/{0}/mock-server:latest', github.repository) || '' }} 80 | ${{ format('ghcr.io/{0}/mock-server:{1}', github.repository, github.ref_name) }} 81 | ${{ env.PUSH_LATEST == 'true' && format('{0}/mcp-gateway-mock-server:latest', env.ALIYUN_REGISTRY) || '' }} 82 | ${{ format('{0}/mcp-gateway-mock-server:{1}', env.ALIYUN_REGISTRY, github.ref_name) }} -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test and Lint 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths-ignore: 7 | - '**.md' 8 | - 'docs/**' 9 | - 'changelog/**' 10 | pull_request: 11 | branches: [ main ] 12 | paths-ignore: 13 | - '**.md' 14 | - 'docs/**' 15 | - 'changelog/**' 16 | 17 | jobs: 18 | web-test-lint: 19 | name: Web Tests and Lint 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: '20.18.0' 29 | cache: 'npm' 30 | cache-dependency-path: 'web/package-lock.json' 31 | 32 | - name: Install dependencies 33 | run: | 34 | cd web 35 | npm ci 36 | 37 | - name: Run ESLint 38 | run: | 39 | cd web 40 | npm run lint 41 | 42 | - name: Build web (dry run) 43 | run: | 44 | cd web 45 | VITE_API_BASE_URL=/api \ 46 | VITE_WS_BASE_URL=/api/ws \ 47 | VITE_BASE_URL=/ \ 48 | VITE_MCP_GATEWAY_BASE_URL=/mcp \ 49 | npm run build 50 | 51 | go-tests: 52 | name: Go Tests 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Checkout code 56 | uses: actions/checkout@v4 57 | 58 | - name: Set up Go 59 | uses: actions/setup-go@v4 60 | with: 61 | go-version: '1.24.1' 62 | cache: true 63 | 64 | - name: Run tests for cmd 65 | run: go test ./cmd/... -v 66 | 67 | - name: Run tests for internal 68 | run: go test ./internal/... -v -race 69 | 70 | - name: Run tests for pkg 71 | run: go test ./pkg/... -v -race 72 | 73 | - name: Generate coverage 74 | run: | 75 | go test ./cmd/... -coverprofile=coverage-cmd.txt -covermode=atomic 76 | go test ./internal/... -coverprofile=coverage-internal.txt -covermode=atomic 77 | go test ./pkg/... -coverprofile=coverage-pkg.txt -covermode=atomic 78 | 79 | - name: Upload coverage report 80 | uses: codecov/codecov-action@v3 81 | with: 82 | files: ./coverage-cmd.txt,./coverage-internal.txt,./coverage-pkg.txt 83 | name: go-coverage 84 | fail_ci_if_error: false 85 | 86 | build-check: 87 | name: Build Check 88 | runs-on: ubuntu-latest 89 | steps: 90 | - name: Checkout code 91 | uses: actions/checkout@v4 92 | 93 | - name: Set up Go 94 | uses: actions/setup-go@v4 95 | with: 96 | go-version: '1.24.1' 97 | 98 | - name: Check if builds succeed 99 | run: | 100 | go build -v ./cmd/apiserver 101 | go build -v ./cmd/mcp-gateway 102 | go build -v ./cmd/mock-server 103 | 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | .env.allinone 27 | .env.multi 28 | .env.development 29 | temp 30 | 31 | .DS_STORE 32 | node_modules 33 | scripts/flow/*/.flowconfig 34 | .flowconfig 35 | *~ 36 | *.pyc 37 | .grunt 38 | _SpecRunner.html 39 | __benchmarks__ 40 | build/ 41 | remote-repo/ 42 | coverage/ 43 | .module-cache 44 | fixtures/dom/public/react-dom.js 45 | fixtures/dom/public/react.js 46 | test/the-files-to-test.generated.js 47 | *.log* 48 | chrome-user-data 49 | *.sublime-project 50 | *.sublime-workspace 51 | .idea 52 | *.iml 53 | .vscode 54 | *.swp 55 | *.swo 56 | 57 | packages/react-devtools-core/dist 58 | packages/react-devtools-extensions/chrome/build 59 | packages/react-devtools-extensions/chrome/*.crx 60 | packages/react-devtools-extensions/chrome/*.pem 61 | packages/react-devtools-extensions/firefox/build 62 | packages/react-devtools-extensions/firefox/*.xpi 63 | packages/react-devtools-extensions/firefox/*.pem 64 | packages/react-devtools-extensions/shared/build 65 | packages/react-devtools-extensions/.tempUserDataDir 66 | packages/react-devtools-fusebox/dist 67 | packages/react-devtools-inline/dist 68 | packages/react-devtools-shell/dist 69 | packages/react-devtools-timeline/dist 70 | 71 | *.pid 72 | data 73 | dist 74 | bin -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 mcp-ecosystem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /changelog/v0.2.10.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.2.10 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 添加OpenAPI转换器 8 | 9 | ## 🛠 架构与代码优化 10 | 11 | - 优化CI流程,将.env打包到zip中 12 | 13 | --- 14 | 15 | 📘 文档:https://mcp.ifuryst.com 16 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 17 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 18 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 19 | 20 | --- 21 | 22 | 感谢所有参与和关注该项目的开发者与用户 💖 23 | 24 | --- 25 | 26 | ## ✨ New Features 27 | 28 | - Add OpenAPI converter 29 | 30 | ## 🛠 Architecture & Code Improvements 31 | 32 | - Optimize CI process, pack .env into zip 33 | 34 | --- 35 | 36 | 📘 Docs: https://mcp.ifuryst.com 37 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 38 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 39 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 40 | 41 | --- 42 | 43 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.2.11.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.2.11 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 添加日志配置功能 8 | - 添加MCP网关代理配置的测试命令和验证功能 9 | 10 | ## 🛠 架构与代码优化 11 | 12 | - 使用无cgo依赖的SQLite库 13 | 14 | --- 15 | 16 | 📘 文档:https://mcp.ifuryst.com 17 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 18 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 19 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 20 | 21 | --- 22 | 23 | 感谢所有参与和关注该项目的开发者与用户 💖 24 | 25 | --- 26 | 27 | ## ✨ New Features 28 | 29 | - Add logger configuration 30 | - Add configuration testing command and validation for MCP gateway proxy configurations 31 | 32 | ## 🛠 Architecture & Code Improvements 33 | 34 | - Use cgo free SQLite library 35 | 36 | --- 37 | 38 | 📘 Docs: https://mcp.ifuryst.com 39 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 40 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 41 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 42 | 43 | --- 44 | 45 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.2.4.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.2.4 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | ### 🧩 多数据库支持扩展 8 | - `Apiserver` 新增 MySQL 支持,适配更多用户场景 9 | - 使用 `GORM` 实现 PostgreSQL 持久化,提升可维护性与扩展性 10 | 11 | ### 🛠 工作流与构建支持增强 12 | - 新增发布二进制文件的 GitHub Actions 工作流 13 | - 支持手动触发 Docker 构建流程 14 | - 添加构建缓存,加速 GitHub Actions 执行 15 | - 构建日志输出到标准输出,方便容器日志收集 16 | 17 | ## ⚙️ 架构与配置优化 18 | 19 | ### 🧱 项目结构重构 20 | - 统一提取 MCP Server 存储逻辑 21 | - 拆分 Chat、MCP、WS 等 Handler,提高代码可读性 22 | - 项目结构更加清晰,模块划分更合理 23 | 24 | ### 📡 通知机制升级 25 | - 新增基于系统信号的配置变更监听器(支持 SIGHUP 重载配置) 26 | 27 | ### 🌐 前端构建兼容优化 28 | - Vite 构建支持相对路径,适配子路径部署场景 29 | 30 | ## 🐞 Bug 修复 31 | 32 | - 修复部分平台下启动时报 illegal seek 的问题 33 | 34 | --- 35 | 36 | 📘 文档:https://mcp.ifuryst.com 37 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 38 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 39 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 40 | 41 | --- 42 | 43 | 感谢所有参与和关注该项目的开发者与用户 💖 44 | 45 | --- 46 | 47 | ## ✨ New Features 48 | 49 | ### 🧩 Multi-Database Support 50 | - `Apiserver` now supports MySQL, enabling broader usage scenarios 51 | - PostgreSQL persistence implemented using `GORM` for better maintainability 52 | 53 | ### 🛠 Workflow & Build Enhancements 54 | - New GitHub Actions workflow to release binaries 55 | - Supports manual trigger for Docker builds 56 | - Build cache enabled for faster CI runs 57 | - Logs output to stdout for easier container logging 58 | 59 | ## ⚙️ Architecture & Config Improvements 60 | 61 | ### 🧱 Project Refactor 62 | - Unified MCP Server storage logic 63 | - Separated Chat, MCP, WS handlers for improved readability 64 | - Clearer and more modular project structure 65 | 66 | ### 📡 Signal-Based Config Watcher 67 | - Added signal-based watcher (SIGHUP) for config reloads 68 | 69 | ### 🌐 Frontend Compatibility Fixes 70 | - Vite now supports relative paths for subpath deployments 71 | 72 | ## 🐞 Bug Fixes 73 | 74 | - Fixed illegal seek error on some platforms 75 | 76 | --- 77 | 78 | 📘 Docs: https://mcp.ifuryst.com 79 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 80 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 81 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 82 | 83 | --- 84 | 85 | Thanks to all contributors and early users! 💖 86 | 87 | -------------------------------------------------------------------------------- /changelog/v0.2.5.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.2.5 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | ### 🗄 MCP Server 配置持久化 8 | - 新增数据库支持,持久化存储 MCP Server 配置信息,提升稳定性与可靠性 9 | 10 | ## 🐞 Bug 修复 11 | 12 | - 修复若干已知问题,提升整体稳定性和体验 13 | 14 | --- 15 | 16 | 📘 文档:https://mcp.ifuryst.com 17 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 18 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 19 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 20 | 21 | --- 22 | 23 | 感谢所有参与和关注该项目的开发者与用户 💖 24 | 25 | --- 26 | 27 | ## ✨ New Features 28 | 29 | ### 🗄 MCP Server Configuration Persistence 30 | - Added database support to persist MCP Server configurations for improved stability and reliability 31 | 32 | ## 🐞 Bug Fixes 33 | 34 | - Fixed several known issues to enhance overall stability and experience 35 | 36 | --- 37 | 38 | 📘 Docs: https://mcp.ifuryst.com 39 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 40 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 41 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 42 | 43 | --- 44 | 45 | Thanks to all contributors and early users! 💖 46 | -------------------------------------------------------------------------------- /changelog/v0.2.6.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.2.6 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | ### 📡 通知系统增强 8 | - 新增 API 通知发送器(API Notifier Sender)支持 9 | - 通知系统支持 Sender、Receiver、双向(Both)多种模式,灵活适配不同场景 10 | 11 | ## 🛠 架构与代码优化 12 | 13 | - 统一提取 Config 和 PID 相关工具函数,提升代码复用性 14 | - 提取 Signal 相关逻辑到 Notifier 模块 15 | - 引入 DB Factory,简化数据库初始化流程 16 | - 重命名 Apiserver 的环境变量,命名更规范清晰 17 | - 优化配置加载逻辑,避免重复读取 18 | - 整理和清理冗余代码,提升可读性和维护性 19 | 20 | ## 🐞 Bug 修复 21 | 22 | - 修复 Supervisord 下 Nginx 日志输出问题,日志更加清晰 23 | 24 | --- 25 | 26 | 📘 文档:https://mcp.ifuryst.com 27 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 28 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 29 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 30 | 31 | --- 32 | 33 | 感谢所有参与和关注该项目的开发者与用户 💖 34 | 35 | --- 36 | 37 | ## ✨ New Features 38 | 39 | ### 📡 Enhanced Notification System 40 | - Added support for API Notifier Sender 41 | - Notification system now supports Sender, Receiver, and Both modes for greater flexibility 42 | 43 | ## 🛠 Architecture & Code Improvements 44 | 45 | - Unified extraction of Config and PID related utility functions 46 | - Moved Signal logic into the Notifier module 47 | - Introduced a DB Factory to streamline database initialization 48 | - Renamed Apiserver environment variables for clearer naming 49 | - Optimized configuration loading to prevent repeated reads 50 | - Cleaned and tidied up redundant codes for better readability and maintainability 51 | 52 | ## 🐞 Bug Fixes 53 | 54 | - Fixed an issue where Supervisord was not properly ignoring Nginx logs, making logs cleaner 55 | 56 | --- 57 | 58 | 📘 Docs: https://mcp.ifuryst.com 59 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 60 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 61 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 62 | 63 | --- 64 | 65 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.2.7.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.2.7 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## 🛠 架构与代码优化 6 | 7 | - 新增多平台二进制发布支持(Windows、macOS) 8 | - 优化二进制部署相关代码 9 | - 添加多平台构建和打包的发布工作流 10 | 11 | ## 🐞 Bug 修复 12 | 13 | - 修复二进制部署相关的问题 14 | 15 | --- 16 | 17 | 📘 文档:https://mcp.ifuryst.com 18 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 19 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 20 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 21 | 22 | --- 23 | 24 | 感谢所有参与和关注该项目的开发者与用户 💖 25 | 26 | --- 27 | 28 | ## 🛠 Architecture & Code Improvements 29 | 30 | - Added multi-platform binary release support (Windows, macOS) 31 | - Optimized code for binary deployment 32 | - Added release workflow for multi-platform builds and packaging 33 | 34 | ## 🐞 Bug Fixes 35 | 36 | - Fixed issues related to binary deployment 37 | 38 | --- 39 | 40 | 📘 Docs: https://mcp.ifuryst.com 41 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 42 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 43 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 44 | 45 | --- 46 | 47 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.2.8.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.2.8 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 添加Redis会话持久化支持 8 | - 添加Product Hunt链接,欢迎点赞支持!❤️ 9 | 10 | ## 🛠 架构与代码优化 11 | 12 | - 代码整理和优化 13 | - 添加优雅关闭功能 14 | 15 | --- 16 | 17 | 📘 文档:https://mcp.ifuryst.com 18 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 19 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 20 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 21 | 22 | --- 23 | 24 | 感谢所有参与和关注该项目的开发者与用户 💖 25 | 26 | --- 27 | 28 | ## ✨ New Features 29 | 30 | - Added Redis session persistence support 31 | - Added Product Hunt link, your upvotes are appreciated! ❤️ 32 | 33 | ## 🛠 Architecture & Code Improvements 34 | 35 | - Code cleanup and optimization 36 | - Added graceful shutdown functionality 37 | 38 | --- 39 | 40 | 📘 Docs: https://mcp.ifuryst.com 41 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 42 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 43 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 44 | 45 | --- 46 | 47 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.2.9.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.2.9 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 支持响应体透传 8 | - 优化错误响应格式 9 | 10 | ## 🛠 架构与代码优化 11 | 12 | - 完善Streamable HTTP响应 13 | - 完善SSE响应 14 | - 代码整理和优化 15 | 16 | --- 17 | 18 | 📘 文档:https://mcp.ifuryst.com 19 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 20 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 21 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 22 | 23 | --- 24 | 25 | 感谢所有参与和关注该项目的开发者与用户 💖 26 | 27 | --- 28 | 29 | ## ✨ New Features 30 | 31 | - Support response body passthrough 32 | - Refine error response format 33 | 34 | ## 🛠 Architecture & Code Improvements 35 | 36 | - Complete streamable HTTP response 37 | - Complete SSE response 38 | - Code cleanup and optimization 39 | 40 | --- 41 | 42 | 📘 Docs: https://mcp.ifuryst.com 43 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 44 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 45 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 46 | 47 | --- 48 | 49 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.3.0.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.3.0 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 实现无缝配置重载,无需中断服务 8 | - 支持参数类型枚举 9 | - 支持处理响应数据 10 | - 增强模板渲染,支持环境变量 11 | - 支持在请求体中注入数组 12 | 13 | ## 🛠 架构与代码优化 14 | 15 | - 代码整理和优化 16 | 17 | --- 18 | 19 | 📘 文档:https://mcp.ifuryst.com 20 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 21 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 22 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 23 | 24 | --- 25 | 26 | 感谢所有参与和关注该项目的开发者与用户 💖 27 | 28 | --- 29 | 30 | ## ✨ New Features 31 | 32 | - Implement seamless configuration reload without service interruption 33 | - Support enum for argument types 34 | - Support processing response data 35 | - Enhance template rendering with environment variable support 36 | - Support array injection in request body 37 | 38 | ## 🛠 Architecture & Code Improvements 39 | 40 | - Code cleanup and optimization 41 | 42 | --- 43 | 44 | 📘 Docs: https://mcp.ifuryst.com 45 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 46 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 47 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 48 | 49 | --- 50 | 51 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.3.1.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.3.1 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 支持折叠MCP工具 8 | - 支持折叠聊天历史 9 | - Web端增加Discord入口 10 | - Web界面优化 11 | - Web增加认证功能 12 | 13 | ## 🛠 架构与代码优化 14 | 15 | - 修复部分小问题,增加mock-stdio-svc.yaml 16 | - 升级Vite依赖 17 | 18 | --- 19 | 20 | 📘 文档:https://mcp.ifuryst.com 21 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 22 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 23 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 24 | 25 | --- 26 | 27 | 感谢所有参与和关注该项目的开发者与用户 💖 28 | 29 | --- 30 | 31 | ## ✨ New Features 32 | 33 | - Support folding MCP tools 34 | - Support folding chat histories 35 | - Add Discord entry to web 36 | - Improve web UI 37 | - Add auth for web 38 | 39 | ## 🛠 Architecture & Code Improvements 40 | 41 | - Fix some small bugs and add mock-stdio-svc.yaml 42 | - Upgrade Vite dependency 43 | 44 | --- 45 | 46 | 📘 Docs: https://mcp.ifuryst.com 47 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 48 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 49 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 50 | 51 | --- 52 | 53 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.3.2.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.3.2 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ⚠️ 重要变更 6 | 7 | 由于新增用户管理功能并调整了超管初始化方式,升级前需要: 8 | 9 | 1. 删除数据库中的 `init_states` 和 `users` 表 10 | 2. 在配置文件中添加以下配置: 11 | 12 | ```yaml 13 | # Super admin configuration 14 | super_admin: 15 | username: "${SUPER_ADMIN_USERNAME:admin}" 16 | password: "${SUPER_ADMIN_PASSWORD:admin}" 17 | ``` 18 | 19 | 环境变量按需增加,强烈建议生产或者公网环境设置随机字符串为密码。 20 | 21 | ## ✨ 新功能 22 | 23 | - 新增用户管理功能 24 | - 添加了i18n支持 25 | - 改进了web UI 26 | - 添加了logo 27 | - 支持修改密码 28 | - 支持折叠MCP工具 29 | - 支持折叠聊天历史 30 | 31 | ## 🐛 修复 32 | 33 | - 修复了聊天界面路由问题 34 | - 修复了登录页面重定向问题 35 | - 优化了错误处理 36 | 37 | --- 38 | 39 | 📘 文档:https://mcp.ifuryst.com 40 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 41 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 42 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 43 | 44 | --- 45 | 46 | 感谢所有参与和关注该项目的开发者与用户 💖 47 | 48 | --- 49 | 50 | ## ⚠️ Breaking Changes 51 | 52 | Due to the addition of user management and changes to super admin initialization, before upgrading: 53 | 54 | 1. Delete the `init_states` and `users` tables from the database 55 | 2. Add the following configuration to your config file: 56 | 57 | ```yaml 58 | # Super admin configuration 59 | super_admin: 60 | username: "${SUPER_ADMIN_USERNAME:admin}" 61 | password: "${SUPER_ADMIN_PASSWORD:admin}" 62 | ``` 63 | 64 | Add environment variables as needed, strongly recommend setting a random string as password for production or public network environments. 65 | 66 | ## ✨ New Features 67 | 68 | - Added user management 69 | - Added i18n support 70 | - Improved web UI 71 | - Added logo 72 | - Support password change 73 | - Support folding MCP tools 74 | - Support folding chat histories 75 | 76 | ## 🐛 Fixes 77 | 78 | - Fixed chat interface routing 79 | - Fixed login page redirect issue 80 | - Enhanced error handling 81 | 82 | --- 83 | 84 | 📘 Docs: https://mcp.ifuryst.com 85 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 86 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 87 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 88 | 89 | --- 90 | 91 | Thanks to all contributors and early users! 💖 92 | -------------------------------------------------------------------------------- /changelog/v0.3.3.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.3.3 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## 🐛 修复 6 | 7 | - 增强了错误信息的国际化支持 8 | - 修复了OpenAPI导入器的问题 9 | - 修复了WebSocket前缀问题 10 | - 修复了属性required的问题 11 | - 优化了supervisord配置以改进日志记录 12 | 13 | --- 14 | 15 | 📘 文档:https://mcp.ifuryst.com 16 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 17 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 18 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 19 | 20 | --- 21 | 22 | 感谢所有参与和关注该项目的开发者与用户 💖 23 | 24 | --- 25 | 26 | ## 🐛 Fixes 27 | 28 | - Enhanced error message internationalization support 29 | - Fixed OpenAPI importer issues 30 | - Fixed WebSocket prefix issue 31 | - Fixed properties required issue 32 | - Optimized supervisord configuration for improved logging 33 | 34 | --- 35 | 36 | 📘 Docs: https://mcp.ifuryst.com 37 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 38 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 39 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 40 | 41 | --- 42 | 43 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.3.4.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.3.4 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 支持form-data格式的请求 8 | - 为Go模板添加了新的函数,支持多种方式提取和转换请求/响应数据 9 | - 添加了微信二维码 10 | - 添加了star history展示 11 | 12 | ## 🔧 改进 13 | 14 | - 重构了Docker镜像配置打包方式 15 | - 支持Docker容器的时区设置 16 | 17 | --- 18 | 19 | 📘 文档:https://mcp.ifuryst.com 20 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 21 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 22 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 23 | 24 | --- 25 | 26 | 感谢所有参与和关注该项目的开发者与用户 💖 27 | 28 | --- 29 | 30 | ## ✨ Features 31 | 32 | - Support form-data format for requests 33 | - Added new functions to Go template for multiple ways of extracting and converting request/response data 34 | - Added WeChat QR code 35 | - Added star history display 36 | 37 | ## 🔧 Improvements 38 | 39 | - Refactored Docker image configuration packaging 40 | - Support timezone for Docker containers 41 | 42 | --- 43 | 44 | 📘 Docs: https://mcp.ifuryst.com 45 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 46 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 47 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 48 | 49 | --- 50 | 51 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.3.5.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.3.5 2 | 3 | > Turn your APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## 🔧 改进 6 | 7 | - 修改了默认日志格式 8 | - 修复了代理相关的bug 9 | 10 | --- 11 | 12 | 📘 文档:https://mcp.ifuryst.com 13 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 14 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 15 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 16 | 17 | --- 18 | 19 | 感谢所有参与和关注该项目的开发者与用户 💖 20 | 21 | --- 22 | 23 | ## 🔧 Improvements 24 | 25 | - Changed default log format 26 | - Fixed proxy-related bugs 27 | 28 | --- 29 | 30 | 📘 Docs: https://mcp.ifuryst.com 31 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 32 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 33 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 34 | 35 | --- 36 | 37 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.4.0.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.4.0 2 | 3 | > Turn your MCP Servers and APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ⚠️ 破坏性升级提醒 6 | 7 | **升级提醒**: 我们强烈建议在升级前备份您的 MCP Gateway 配置,升级后再重新配置进去,可以较为丝滑地完成升级。 8 | 9 | ## ✨ 新功能 10 | 11 | - 支持代理MCP服务,Client->MCP Gateway->MCP Servers 12 | - 支持SSE和Streamable HTTP到stdio, SSE, Streamable HTTP的代理 13 | - 在网关页面中支持显示stdio、SSE和Streamable HTTP的详细信息 14 | - 增加租户管理功能 15 | - 增加租户CURD管理 16 | - 将租户关联到用户 17 | - 添加网关管理器中的租户选择器 18 | - 添加路由器和租户之间前缀的验证 19 | - mcp-gateway支持主动API拉取配置功能 20 | - 支持MCP中响应图像、音频内容结果(原来只支持文本) 21 | - 为apiserver和web添加国际化(i18n)支持 22 | - 添加Redis用户名支持 23 | - 添加健康检查URL用于k8s探针 24 | 25 | ## 🔧 改进 26 | 27 | - 增强日志配置并添加时区配置 28 | - 调整项目结构并删除无用代码 29 | 30 | --- 31 | 32 | 📘 文档:https://mcp.ifuryst.com 33 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 34 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 35 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 36 | 🔗 扫描下方二维码加入社区微信群,备注:`mcp-gateway`或`mcpgw` 37 | 微信群二维码 38 | 39 | --- 40 | 41 | 感谢所有参与和关注该项目的开发者与用户 💖 42 | 43 | --- 44 | 45 | ## ⚠️ Breaking Changes 46 | 47 | **Upgrade Notice**: We strongly recommend backing up your MCP Gateway configuration before upgrading, then reconfiguring after the update for a smooth upgrade experience. 48 | 49 | ## ✨ New Features 50 | 51 | - Support for MCP service proxying, Client->MCP Gateway->MCP Servers 52 | - Support for SSE and Streamable HTTP to stdio, SSE, Streamable HTTP proxying 53 | - Support for displaying stdio, SSE, and Streamable HTTP details in the gateway page 54 | - Enhanced tenant management 55 | - Added tenant CRUD management 56 | - Attached tenants to user 57 | - Added tenant selector to gateway manager 58 | - Added validation for prefix between router and tenant 59 | - Added API configuration fetching capability to mcp-gateway 60 | - Support for image and audio content results in MCP responses (previously text-only) 61 | - Added i18n internationalization support for both apiserver and web 62 | - Added Redis username support 63 | - Added health_check URL for k8s probe 64 | 65 | ## 🔧 Improvements 66 | 67 | - Enhanced logger configuration and added timezone config 68 | - Adjusted structure and deleted useless code 69 | 70 | --- 71 | 72 | 📘 Docs: https://mcp.ifuryst.com 73 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 74 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 75 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 76 | 🔗 Scan the QR code below to join WeChat community group, note: `mcp-gateway` or `mcpgw` 77 | WeChat QR Code 78 | 79 | --- 80 | 81 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.4.1.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.4.1 2 | 3 | > Turn your MCP Servers and APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 支持 toJSON 转换对象或数组为 JSON 8 | - 支持在API通知种传递更新的配置 9 | - 支持自定义请求中的 headers/cookies/querystring 信息合并 10 | 11 | ## 🔧 改进 12 | 13 | - 为导入的配置添加随机后缀 14 | - 改进web里组件的可访问性 15 | - 增强日志记录 16 | - 合并 multi-container 里 apiserver 和 web 到一个 Docker 镜像 17 | - allinone 镜像里使用 uv, pipx 和 node 环境 18 | 19 | ## 🐞 修复 20 | 21 | - 修复当网关存储为数据库时租户tenant字段缺失的问题 22 | - 修复合并过程中处理 nil 更新配置的问题 23 | 24 | --- 25 | 26 | 📘 文档:https://mcp.ifuryst.com 27 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 28 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 29 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 30 | 🔗 扫描下方二维码加入社区微信群,备注:`mcp-gateway`或`mcpgw` 31 | 微信群二维码 32 | 33 | --- 34 | 35 | 感谢所有参与和关注该项目的开发者与用户 💖 36 | 37 | --- 38 | 39 | ## ✨ New Features 40 | 41 | - Support toJSON to convert object or array to JSON 42 | - Support passing updated configurations in API notifications 43 | - Support for merging headers/cookies/querystring information in custom requests 44 | 45 | ## 🔧 Improvements 46 | 47 | - Added random suffix for imported configurations 48 | - Improved web component accessibility 49 | - Enhanced logging 50 | - Merged apiserver and web into one Docker image for multi-container setup 51 | - Using uv, pipx and node environment in allinone image 52 | 53 | ## 🐞 Bug Fixes 54 | 55 | - Fixed tenant field missing when gateway storage is database 56 | - Fixed handling of nil updated configuration in merge process 57 | 58 | --- 59 | 60 | 📘 Docs: https://mcp.ifuryst.com 61 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 62 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 63 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 64 | 🔗 Scan the QR code below to join WeChat community group, note: `mcp-gateway` or `mcpgw` 65 | WeChat QR Code 66 | 67 | --- 68 | 69 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.4.2.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.4.2 2 | 3 | > Turn your MCP Servers and APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 增强GatewayManager,添加视图模式和模态框支持 8 | 9 | ## 🔧 改进 10 | 11 | - 在Dockerfile中使用基础镜像并移除不必要的包安装 12 | - 添加Dockerfile和GitHub Actions工作流用于构建和发布基础镜像 13 | - 支持从环境变量配置pip、uv和npm源 14 | 15 | ## 🐞 修复 16 | 17 | - 验证MCPGatewayConfig的ReloadInterval确保大于0 18 | - 移除MySQL配置中MCPConfig字段的默认值 19 | 20 | --- 21 | 22 | 📘 文档:https://mcp.ifuryst.com 23 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 24 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 25 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 26 | 🔗 扫描下方二维码加入社区微信群,备注:`mcp-gateway`或`mcpgw` 27 | 微信群二维码 28 | 29 | --- 30 | 31 | 感谢所有参与和关注该项目的开发者与用户 💖 32 | 33 | --- 34 | 35 | ## ✨ New Features 36 | 37 | - Enhance GatewayManager with view modes and modals 38 | 39 | ## 🔧 Improvements 40 | 41 | - Update Dockerfile to use base image and remove unnecessary package installations 42 | - Add Dockerfile and GitHub Actions workflow for building and publishing base image 43 | - Add pip, uv and npm source configured from environment variables 44 | 45 | ## 🐞 Bug Fixes 46 | 47 | - Validate MCPGatewayConfig ReloadInterval to ensure it's greater than 0 48 | - Remove default values from MCPConfig fields for MySQL 49 | 50 | --- 51 | 52 | 📘 Docs: https://mcp.ifuryst.com 53 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 54 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 55 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 56 | 🔗 Scan the QR code below to join WeChat community group, note: `mcp-gateway` or `mcpgw` 57 | WeChat QR Code 58 | 59 | --- 60 | 61 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.4.3.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.4.3 2 | 3 | > Turn your MCP Servers and APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 支持OpenAPI 3.1和Swagger 2.0转换功能 8 | - 添加用户友好的网关配置编辑模式 9 | - 添加Redis会话存储的前缀配置 10 | - 添加issue模板,支持bug报告、功能请求、讨论和提案 11 | 12 | --- 13 | 14 | 📘 文档:https://mcp.ifuryst.com 15 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 16 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 17 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 18 | 🔗 扫描下方二维码加入社区微信群,备注:`mcp-gateway`或`mcpgw` 19 | 微信群二维码 20 | 21 | --- 22 | 23 | 感谢所有参与和关注该项目的开发者与用户 💖 24 | 25 | --- 26 | 27 | ## ✨ New Features 28 | 29 | - Support OpenAPI 3.1 and Swagger 2.0 conversion 30 | - Add user-friendly gateway configuration edit mode 31 | - Add prefix configuration to Redis session store 32 | - Add issue templates for bug reports, feature requests, discussions, and proposals 33 | 34 | --- 35 | 36 | 📘 Docs: https://mcp.ifuryst.com 37 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 38 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 39 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 40 | 🔗 Scan the QR code below to join WeChat community group, note: `mcp-gateway` or `mcpgw` 41 | WeChat QR Code 42 | 43 | --- 44 | 45 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.4.4.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.4.4 2 | 3 | > Turn your MCP Servers and APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 添加MCP服务器启动策略配置 8 | - 添加MCP服务器导出功能 9 | - mock服务支持STDIO和SSE 10 | - 添加zod依赖用于模式验证 11 | 12 | ## 🔧 优化 13 | 14 | - 优化MCP代理相关代码 15 | - 简化传输实现,移除不必要的MCP服务器配置 16 | 17 | ## 🐛 修复 18 | 19 | - 添加MCPConfig的json标签,并统一小写驼峰 20 | 21 | --- 22 | 23 | 📘 文档:https://mcp.ifuryst.com 24 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 25 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 26 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 27 | 🔗 扫描下方二维码加入社区微信群,备注:`mcp-gateway`或`mcpgw` 28 | 微信群二维码 29 | 30 | --- 31 | 32 | 感谢所有参与和关注该项目的开发者与用户 💖 33 | 34 | --- 35 | 36 | ## ✨ New Features 37 | 38 | - Add startup policy configuration for MCP servers 39 | - Add export functionality for MCP servers 40 | - Add STDIO and SSE support for mock services 41 | - Add zod dependency for schema validation 42 | 43 | ## 🔧 Optimizations 44 | 45 | - Optimize MCP proxy related codes 46 | - Simplify transport implementations by removing unnecessary MCP server configuration 47 | 48 | ## 🐛 Fixes 49 | 50 | - Add MCPConfig json tag and unify to camelCase 51 | 52 | --- 53 | 54 | 📘 Docs: https://mcp.ifuryst.com 55 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 56 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 57 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 58 | 🔗 Scan the QR code below to join WeChat community group, note: `mcp-gateway` or `mcpgw` 59 | WeChat QR Code 60 | 61 | --- 62 | 63 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.4.5.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.4.5 2 | 3 | > Turn your MCP Servers and APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 支持参数默认值配置 8 | - 添加MCP服务预安装配置选项 9 | - 增加GitHub Actions CI工作流 10 | 11 | ## 🔧 优化 12 | 13 | - 调整代码结构和组织 14 | - 优化Docker构建配置 15 | 16 | ## 🐛 修复 17 | 18 | - 修复zap日志配置中的AddCallerSkip选项 19 | - 修复web端lint问题 20 | - 修复MCPGatewayConfig类型断言问题 21 | - 修复base_url相关问题 22 | 23 | --- 24 | 25 | 📘 文档:https://mcp.ifuryst.com 26 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 27 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 28 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 29 | 🔗 扫描下方二维码加入社区微信群,备注:`mcp-gateway`或`mcpgw` 30 | 微信群二维码 31 | 32 | --- 33 | 34 | 感谢所有参与和关注该项目的开发者与用户 💖 35 | 36 | --- 37 | 38 | ## ✨ New Features 39 | 40 | - Support default value configuration for parameters 41 | - Add pre-installation configuration options for MCP services 42 | - Add GitHub Actions CI workflow 43 | 44 | ## 🔧 Optimizations 45 | 46 | - Adjust code structure and organization 47 | - Optimize Docker build configuration 48 | 49 | ## 🐛 Fixes 50 | 51 | - Fix AddCallerSkip option in zap logger configuration 52 | - Fix web lint issues 53 | - Fix type assertion for MCPGatewayConfig 54 | - Fix base_url related issues 55 | 56 | --- 57 | 58 | 📘 Docs: https://mcp.ifuryst.com 59 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 60 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 61 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 62 | 🔗 Scan the QR code below to join WeChat community group, note: `mcp-gateway` or `mcpgw` 63 | WeChat QR Code 64 | 65 | --- 66 | 67 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.4.6.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.4.6 2 | 3 | > Turn your MCP Servers and APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ⚠️ Breaking Changes 6 | 7 | - MCP Gateway核心配置同步逻辑调整,大部分情况可丝滑升级,部分场景可能存在升级风险,请注意关注升级后的兼容性 8 | 9 | ## 🔧 优化 10 | 11 | - 优化网关管理页面布局和响应式设计 12 | - 重构服务器配置管理,整合重载和更新逻辑 13 | - 简化状态管理,引入getter方法 14 | - 增强MCP服务代理配置删除处理 15 | - 添加复制MCP URL按钮功能 16 | 17 | --- 18 | 19 | 📘 文档:https://mcp.ifuryst.com 20 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 21 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 22 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 23 | 🔗 扫描下方二维码加入社区微信群,备注:`mcp-gateway`或`mcpgw` 24 | 微信群二维码 25 | 26 | --- 27 | 28 | 感谢所有参与和关注该项目的开发者与用户 💖 29 | 30 | --- 31 | 32 | ## ⚠️ Breaking Changes 33 | 34 | - MCP Gateway core configuration synchronization logic has been adjusted. While most cases should upgrade smoothly, some scenarios may have upgrade risks. Please pay attention to compatibility after upgrading. 35 | 36 | ## 🔧 Optimizations 37 | 38 | - Optimize gateway manager page layout and responsive design 39 | - Refactor server configuration management, consolidate reload and update logic 40 | - Simplify state management by introducing getter methods 41 | - Enhance MCP service proxy configuration deletion handling 42 | - Add button to copy MCP URL 43 | 44 | --- 45 | 46 | 📘 Docs: https://mcp.ifuryst.com 47 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 48 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 49 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 50 | 🔗 Scan the QR code below to join WeChat community group, note: `mcp-gateway` or `mcpgw` 51 | WeChat QR Code 52 | 53 | --- 54 | 55 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.4.7.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.4.7 2 | 3 | > Turn your MCP Servers and APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 添加MCP配置版本控制功能 8 | - 实现MultiSelectAutocomplete组件并集成到用户管理 9 | 10 | ## 🔧 优化 11 | 12 | - 优化网关管理UI和UX 13 | - 更新Dockerfile基础镜像,应对安全问题 14 | 15 | ## 🐛 修复 16 | 17 | - 修复SSE和Streamable HTTP URL的复制功能 18 | - 防止保存null值 19 | 20 | ## 🔨 其他 21 | 22 | - 删除namespace字段 23 | - 添加Snyk徽章 24 | - 添加CodeQL分析工作流用于自动化代码扫描 25 | - 添加Go Report Card徽章到README 26 | 27 | --- 28 | 29 | 📘 文档:https://mcp.ifuryst.com 30 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 31 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 32 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 33 | 🔗 扫描下方二维码加入社区微信群,备注:`mcp-gateway`或`mcpgw` 34 | 微信群二维码 35 | 36 | --- 37 | 38 | 感谢所有参与和关注该项目的开发者与用户 💖 39 | 40 | --- 41 | 42 | ## ✨ New Features 43 | 44 | - Add MCP configuration versioning 45 | - Implement MultiSelectAutocomplete component and integrate it into user management 46 | 47 | ## 🔧 Optimizations 48 | 49 | - Enhance UI and UX for gateway management 50 | - Update Dockerfile base image to address security concerns 51 | 52 | ## 🐛 Fixes 53 | 54 | - Fix copy functionality for SSE and Streamable HTTP URLs 55 | - Prevent saving null values 56 | 57 | ## 🔨 Others 58 | 59 | - Delete namespace field 60 | - Add Snyk badge 61 | - Add CodeQL analysis workflow for automated code scanning 62 | - Add Go Report Card badge to README 63 | 64 | --- 65 | 66 | 📘 Docs: https://mcp.ifuryst.com 67 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 68 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 69 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 70 | 🔗 Scan the QR code below to join WeChat community group, note: `mcp-gateway` or `mcpgw` 71 | WeChat QR Code 72 | 73 | --- 74 | 75 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.5.0.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.5.0 2 | 3 | > Turn your MCP Servers and APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ⚠️ 破坏性更新 6 | 7 | 配置相关的数据表结构发生变更,升级时请先备份配置数据,然后删除旧表并重建表结构,这样可以相对无痛地进行升级 8 | 9 | ## ✨ 新功能 10 | 11 | - 实现MCP配置版本管理功能 12 | - 增强租户管理和权限处理 13 | - 添加MCP配置的软删除支持 14 | - 增强MCP配置增量更新逻辑 15 | 16 | ## 🔧 优化 17 | 18 | - 优化前端UI样式 19 | - 统一sqlite数据库路径 20 | - 优化数据表的字段长度和索引 21 | 22 | ## 🐛 修复 23 | 24 | - 更新Dockerfile中的启动脚本,使用/bin/sh替代/bin/bash 25 | - 修复MySQL中name字段的索引问题 26 | 27 | ## 🔨 其他 28 | 29 | - 添加缺失的web i18n消息 30 | - 添加cursor bg环境文件 31 | 32 | --- 33 | 34 | 📘 文档:https://mcp.ifuryst.com 35 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 36 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 37 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 38 | 🔗 扫描下方二维码加入社区微信群,备注:`mcp-gateway`或`mcpgw` 39 | 微信群二维码 40 | 41 | --- 42 | 43 | 感谢所有参与和关注该项目的开发者与用户 💖 44 | 45 | --- 46 | 47 | ## ⚠️ Breaking Changes 48 | 49 | Configuration-related database tables have been modified. Before upgrading, please backup your configuration data, then drop the old tables and recreate them for a relatively painless upgrade process 50 | 51 | ## ✨ New Features 52 | 53 | - Implement MCP configuration versioning 54 | - Enhance tenant management and permissions handling 55 | - Add soft deletion support for MCP configuration 56 | - Enhance MCP configuration incremental update logic 57 | 58 | ## 🔧 Optimizations 59 | 60 | - Optimize frontend UI styles 61 | - Unify SQLite database path 62 | - Optimize database table field lengths and indexes 63 | 64 | ## 🐛 Fixes 65 | 66 | - Update startup script in Dockerfile to use /bin/sh instead of /bin/bash 67 | - Fix MySQL name field index issue 68 | 69 | ## 🔨 Others 70 | 71 | - Add missing web i18n messages 72 | - Add cursor bg env file 73 | 74 | --- 75 | 76 | 📘 Docs: https://mcp.ifuryst.com 77 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 78 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 79 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 80 | 🔗 Scan the QR code below to join WeChat community group, note: `mcp-gateway` or `mcpgw` 81 | WeChat QR Code 82 | 83 | --- 84 | 85 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /changelog/v0.5.1.md: -------------------------------------------------------------------------------- 1 | # 📦 MCP Gateway v0.5.1 2 | 3 | > Turn your MCP Servers and APIs into MCP endpoints — effortlessly, without modifying the original code. 4 | 5 | ## ✨ 新功能 6 | 7 | - 添加代理配置的修订历史限制功能 8 | - 添加聊天会话管理功能,包括删除和更新标题 9 | - 为 Redis 中的键添加 TTL 支持 10 | 11 | ## 🐛 修复 12 | 13 | - 修复 header 参数问题 14 | - 回滚 web 基础镜像 15 | 16 | --- 17 | 18 | 📘 文档:https://mcp.ifuryst.com 19 | 🐙 源码:https://github.com/mcp-ecosystem/mcp-gateway 20 | 🐳 Docker 镜像:`ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 21 | 💬 加入我们的 Discord 社区参与讨论:https://discord.gg/udf69cT9TY 22 | 🔗 扫描下方二维码加入社区微信群,备注:`mcp-gateway`或`mcpgw` 23 | 微信群二维码 24 | 25 | --- 26 | 27 | 感谢所有参与和关注该项目的开发者与用户 💖 28 | 29 | --- 30 | 31 | ## ✨ New Features 32 | 33 | - Add revision history limit to proxy configuration 34 | - Add chat session management features including delete and update title 35 | - Add TTL for keys in Redis 36 | 37 | ## 🐛 Fixes 38 | 39 | - Fix header arguments issue 40 | - Rollback web base image 41 | 42 | --- 43 | 44 | 📘 Docs: https://mcp.ifuryst.com 45 | 🐙 Source: https://github.com/mcp-ecosystem/mcp-gateway 46 | 🐳 Docker Image: `ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest` 47 | 💬 Join our Discord community for discussions: https://discord.gg/udf69cT9TY 48 | 🔗 Scan the QR code below to join WeChat community group, note: `mcp-gateway` or `mcpgw` 49 | WeChat QR Code 50 | 51 | --- 52 | 53 | Thanks to all contributors and early users! 💖 -------------------------------------------------------------------------------- /cmd/openapi-converter/examples/mock-user-svc.v3.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Mock User Service API 4 | version: 1.0.0 5 | description: A mock user management service. 6 | servers: 7 | - url: http://localhost:5236 8 | paths: 9 | /users: 10 | post: 11 | summary: Create a new user 12 | requestBody: 13 | required: true 14 | content: 15 | application/json: 16 | schema: 17 | $ref: '#/components/schemas/User' 18 | responses: 19 | '201': 20 | description: User created 21 | content: 22 | application/json: 23 | schema: 24 | $ref: '#/components/schemas/User' 25 | '400': 26 | description: Invalid request 27 | /users/email/{email}: 28 | get: 29 | summary: Get user by email 30 | parameters: 31 | - name: email 32 | in: path 33 | required: true 34 | schema: 35 | type: string 36 | responses: 37 | '200': 38 | description: User found 39 | content: 40 | application/json: 41 | schema: 42 | $ref: '#/components/schemas/User' 43 | '404': 44 | description: User not found 45 | /users/{email}/preferences: 46 | put: 47 | summary: Update user preferences 48 | parameters: 49 | - name: email 50 | in: path 51 | required: true 52 | schema: 53 | type: string 54 | requestBody: 55 | required: true 56 | content: 57 | application/json: 58 | schema: 59 | $ref: '#/components/schemas/UserPreferences' 60 | responses: 61 | '200': 62 | description: Preferences updated 63 | content: 64 | application/json: 65 | schema: 66 | $ref: '#/components/schemas/User' 67 | '400': 68 | description: Invalid request 69 | '404': 70 | description: User not found 71 | components: 72 | schemas: 73 | User: 74 | type: object 75 | properties: 76 | id: 77 | type: string 78 | username: 79 | type: string 80 | email: 81 | type: string 82 | createdAt: 83 | type: string 84 | format: date-time 85 | preferences: 86 | $ref: '#/components/schemas/UserPreferences' 87 | UserPreferences: 88 | type: object 89 | properties: 90 | isPublic: 91 | type: boolean 92 | description: Whether the user profile is public 93 | showEmail: 94 | type: boolean 95 | description: Whether to show email in profile 96 | theme: 97 | type: string 98 | description: User interface theme 99 | enum: [light, dark, system] 100 | tags: 101 | type: array 102 | items: 103 | type: string 104 | description: User tags 105 | -------------------------------------------------------------------------------- /cmd/openapi-converter/examples/petstore.v3.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Petstore API 4 | description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI specification 5 | version: 1.0.0 6 | servers: 7 | - url: https://petstore.example.com/v1 8 | paths: 9 | /pets: 10 | get: 11 | summary: List all pets 12 | operationId: listPets 13 | responses: 14 | '200': 15 | description: A paged array of pets 16 | post: 17 | summary: Create a pet 18 | operationId: createPet 19 | responses: 20 | '201': 21 | description: Pet created 22 | options: 23 | summary: CORS support 24 | responses: 25 | '200': 26 | description: CORS headers 27 | /pets/{petId}: 28 | get: 29 | summary: Info for a specific pet 30 | operationId: showPetById 31 | parameters: 32 | - name: petId 33 | in: path 34 | required: true 35 | schema: 36 | type: string 37 | responses: 38 | '200': 39 | description: Expected response to a valid request 40 | '404': 41 | description: Pet not found 42 | options: 43 | summary: CORS support 44 | parameters: 45 | - name: petId 46 | in: path 47 | required: true 48 | schema: 49 | type: string 50 | responses: 51 | '200': 52 | description: CORS headers -------------------------------------------------------------------------------- /cmd/openapi-converter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/mcp-ecosystem/mcp-gateway/pkg/openapi" 12 | "github.com/spf13/cobra" 13 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | var ( 17 | outputFormat string 18 | outputFile string 19 | 20 | rootCmd = &cobra.Command{ 21 | Use: "openapi-converter [openapi-file]", 22 | Short: "Convert OpenAPI specification to MCP Gateway configuration", 23 | Long: `openapi-converter is a tool to convert OpenAPI specifications (JSON or YAML) 24 | to MCP Gateway configuration format. It can read from a file or standard input 25 | and output the result to a file or standard output.`, 26 | Args: cobra.ExactArgs(1), 27 | RunE: func(cmd *cobra.Command, args []string) error { 28 | // Create converter 29 | converter := openapi.NewConverter() 30 | 31 | // Read input file 32 | var input []byte 33 | var err error 34 | if args[0] == "-" { 35 | input, err = io.ReadAll(os.Stdin) 36 | } else { 37 | input, err = os.ReadFile(args[0]) 38 | } 39 | if err != nil { 40 | return fmt.Errorf("failed to read input: %w", err) 41 | } 42 | 43 | // Convert based on file extension 44 | var config interface{} 45 | ext := strings.ToLower(filepath.Ext(args[0])) 46 | switch ext { 47 | case ".json": 48 | config, err = converter.ConvertFromJSON(input) 49 | case ".yaml", ".yml": 50 | config, err = converter.ConvertFromYAML(input) 51 | default: 52 | // Try JSON first, then YAML 53 | config, err = converter.ConvertFromJSON(input) 54 | if err != nil { 55 | config, err = converter.ConvertFromYAML(input) 56 | } 57 | } 58 | if err != nil { 59 | return fmt.Errorf("failed to convert: %w", err) 60 | } 61 | 62 | // Marshal output 63 | var output []byte 64 | switch outputFormat { 65 | case "json": 66 | output, err = json.MarshalIndent(config, "", " ") 67 | case "yaml": 68 | output, err = yaml.Marshal(config) 69 | default: 70 | return fmt.Errorf("unsupported output format: %s", outputFormat) 71 | } 72 | if err != nil { 73 | return fmt.Errorf("failed to marshal output: %w", err) 74 | } 75 | 76 | // Write output 77 | if outputFile == "" { 78 | fmt.Println(string(output)) 79 | } else { 80 | if err := os.WriteFile(outputFile, output, 0644); err != nil { 81 | return fmt.Errorf("failed to write output: %w", err) 82 | } 83 | } 84 | 85 | return nil 86 | }, 87 | } 88 | ) 89 | 90 | func init() { 91 | rootCmd.Flags().StringVarP(&outputFormat, "format", "f", "yaml", "Output format (json or yaml)") 92 | rootCmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output file (default: stdout)") 93 | } 94 | 95 | func main() { 96 | if err := rootCmd.Execute(); err != nil { 97 | _, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", err) 98 | os.Exit(1) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /configs/proxy-mcp-exp.yaml: -------------------------------------------------------------------------------- 1 | name: "proxy-mcp-exp" 2 | tenant: "default" 3 | 4 | routers: 5 | - server: "amap-maps" 6 | prefix: "/mcp/stdio-proxy" 7 | cors: 8 | allowOrigins: 9 | - "*" 10 | allowMethods: 11 | - "GET" 12 | - "POST" 13 | - "OPTIONS" 14 | allowHeaders: 15 | - "Content-Type" 16 | - "Authorization" 17 | - "Mcp-Session-Id" 18 | exposeHeaders: 19 | - "Mcp-Session-Id" 20 | allowCredentials: true 21 | - server: "mock-user-sse" 22 | prefix: "/mcp/sse-proxy" 23 | cors: 24 | allowOrigins: 25 | - "*" 26 | allowMethods: 27 | - "GET" 28 | - "POST" 29 | - "OPTIONS" 30 | allowHeaders: 31 | - "Content-Type" 32 | - "Authorization" 33 | - "Mcp-Session-Id" 34 | exposeHeaders: 35 | - "Mcp-Session-Id" 36 | allowCredentials: true 37 | - server: "mock-user-mcp" 38 | prefix: "/mcp/streamable-http-proxy" 39 | cors: 40 | allowOrigins: 41 | - "*" 42 | allowMethods: 43 | - "GET" 44 | - "POST" 45 | - "OPTIONS" 46 | allowHeaders: 47 | - "Content-Type" 48 | - "Authorization" 49 | - "Mcp-Session-Id" 50 | exposeHeaders: 51 | - "Mcp-Session-Id" 52 | allowCredentials: true 53 | 54 | mcpServers: 55 | - type: "stdio" 56 | name: "amap-maps" 57 | command: "npx" 58 | args: 59 | - "-y" 60 | - "@amap/amap-maps-mcp-server" 61 | env: 62 | AMAP_MAPS_API_KEY: '{{ env "AMAP_MAPS_API_KEY" }}' 63 | policy: "onDemand" 64 | preinstalled: true 65 | 66 | - type: "sse" 67 | name: "mock-user-sse" 68 | url: "http://localhost:3000/mcp/user/sse" 69 | policy: "onDemand" 70 | 71 | - type: "streamable-http" # unimplemented for now 72 | name: "mock-user-mcp" 73 | url: "http://localhost:3000/mcp/user/mcp" 74 | policy: "onDemand" 75 | -------------------------------------------------------------------------------- /deploy/docker/allinone/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.1 AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod go.sum ./ 6 | 7 | RUN go mod download 8 | 9 | COPY . . 10 | 11 | RUN GOOS=linux go build -o /app/bin/apiserver ./cmd/apiserver 12 | RUN GOOS=linux go build -o /app/bin/mcp-gateway ./cmd/mcp-gateway 13 | RUN GOOS=linux go build -o /app/bin/mock-server ./cmd/mock-server 14 | 15 | FROM node:20.18.0 AS web-builder 16 | 17 | ARG VITE_API_BASE_URL=/api 18 | ARG VITE_WS_BASE_URL=/api/ws 19 | ARG VITE_MCP_GATEWAY_BASE_URL=/mcp 20 | ARG VITE_BASE_URL=/ 21 | 22 | ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} \ 23 | VITE_WS_BASE_URL=${VITE_WS_BASE_URL} \ 24 | VITE_MCP_GATEWAY_BASE_URL=${VITE_MCP_GATEWAY_BASE_URL} \ 25 | VITE_BASE_URL=${VITE_BASE_URL} 26 | 27 | WORKDIR /app/web 28 | 29 | COPY web/package*.json ./ 30 | 31 | RUN npm install 32 | 33 | COPY web/ . 34 | 35 | RUN npm run build 36 | 37 | FROM ghcr.io/mcp-ecosystem/mcp-gateway/base:latest AS runtime 38 | 39 | ARG PIP_INDEX_URL=https://pypi.org/simple 40 | ARG UV_DEFAULT_INDEX=https://pypi.org/simple 41 | ARG npm_config_registry=https://registry.npmjs.org/ 42 | 43 | ENV PIP_INDEX_URL=${PIP_INDEX_URL} \ 44 | UV_DEFAULT_INDEX=${UV_DEFAULT_INDEX} \ 45 | npm_config_registry=${npm_config_registry} 46 | 47 | # Configure pip and npm to use the specified repositories 48 | RUN mkdir -p /root/.config/pip && \ 49 | echo "[global]\nindex-url = \${PIP_INDEX_URL}" > /root/.config/pip/pip.conf && \ 50 | npm config set registry ${npm_config_registry} -g 51 | 52 | COPY deploy/docker/allinone/supervisord.conf /etc/supervisor/conf.d/ 53 | COPY deploy/docker/allinone/nginx.conf /etc/nginx/nginx.conf 54 | RUN mkdir -p /app/data 55 | COPY configs/apiserver.yaml /etc/mcp-gateway/ 56 | COPY configs/mcp-gateway.yaml /etc/mcp-gateway/ 57 | COPY configs/i18n /app/configs/i18n/ 58 | 59 | COPY --from=builder /app/bin/mcp-gateway /usr/local/bin/ 60 | COPY --from=builder /app/bin/mock-server /usr/local/bin/ 61 | COPY --from=builder /app/bin/apiserver /usr/local/bin/ 62 | 63 | COPY --from=web-builder /app/web/dist /usr/share/nginx/html 64 | 65 | EXPOSE 80 66 | 67 | CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/supervisord.conf"] -------------------------------------------------------------------------------- /deploy/docker/allinone/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mcp-gateway: 3 | image: ${IMAGE:-ghcr.io/mcp-ecosystem/mcp-gateway/allinone:latest} 4 | ports: 5 | - "8080:80" 6 | - "5234:5234" 7 | - "5235:5235" 8 | - "5236:5236" 9 | environment: 10 | - ENV=production 11 | - TZ=${TZ:-UTC} 12 | volumes: 13 | - ./configs:/app/configs 14 | - ./data:/app/data 15 | - ./.env.allinone:/app/.env 16 | restart: unless-stopped 17 | -------------------------------------------------------------------------------- /deploy/docker/allinone/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | include /etc/nginx/mime.types; 7 | default_type application/octet-stream; 8 | 9 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 10 | '$status $body_bytes_sent "$http_referer" ' 11 | '"$http_user_agent" "$http_x_forwarded_for"'; 12 | access_log /dev/stdout main; 13 | error_log /dev/stderr; 14 | 15 | # API Server 16 | upstream apiserver { 17 | server localhost:5234; 18 | } 19 | 20 | # MCP Gateway 21 | upstream mcp-gateway { 22 | server localhost:5235; 23 | } 24 | 25 | server { 26 | listen 80; 27 | server_name localhost; 28 | 29 | location / { 30 | root /usr/share/nginx/html; 31 | try_files $uri $uri/ /index.html; 32 | } 33 | 34 | location /api/ { 35 | proxy_pass http://apiserver; 36 | proxy_set_header Host $host; 37 | proxy_set_header X-Real-IP $remote_addr; 38 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 39 | proxy_set_header X-Forwarded-Proto $scheme; 40 | } 41 | 42 | location /api/ws/ { 43 | proxy_pass http://apiserver; 44 | proxy_http_version 1.1; 45 | proxy_set_header Upgrade $http_upgrade; 46 | proxy_set_header Connection "upgrade"; 47 | proxy_set_header Host $host; 48 | proxy_set_header X-Real-IP $remote_addr; 49 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 50 | proxy_set_header X-Forwarded-Proto $scheme; 51 | } 52 | 53 | location /mcp/ { 54 | proxy_pass http://mcp-gateway/; 55 | proxy_set_header Host $host; 56 | proxy_set_header X-Real-IP $remote_addr; 57 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 58 | proxy_set_header X-Forwarded-Proto $scheme; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /deploy/docker/allinone/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | logfile=/dev/stdout 4 | logfile_maxbytes=0 5 | pidfile=/var/run/supervisord.pid 6 | user=root 7 | 8 | [program:mcp-gateway] 9 | command=/usr/local/bin/mcp-gateway 10 | directory=/app 11 | autostart=true 12 | autorestart=true 13 | stdout_logfile=/dev/fd/1 14 | stderr_logfile=/dev/fd/2 15 | stdout_logfile_maxbytes=0 16 | stderr_logfile_maxbytes=0 17 | 18 | [program:mock-server] 19 | command=/usr/local/bin/mock-server 20 | directory=/app 21 | autostart=true 22 | autorestart=true 23 | stdout_logfile=/dev/fd/1 24 | stderr_logfile=/dev/fd/2 25 | stdout_logfile_maxbytes=0 26 | stderr_logfile_maxbytes=0 27 | 28 | [program:apiserver] 29 | command=/usr/local/bin/apiserver 30 | directory=/app 31 | autostart=true 32 | autorestart=true 33 | stdout_logfile=/dev/fd/1 34 | stderr_logfile=/dev/fd/2 35 | stdout_logfile_maxbytes=0 36 | stderr_logfile_maxbytes=0 37 | 38 | [program:nginx] 39 | command=/usr/sbin/nginx -g "daemon off;" 40 | autostart=true 41 | autorestart=true 42 | stdout_logfile=/dev/fd/1 43 | stderr_logfile=/dev/fd/2 44 | stdout_logfile_maxbytes=0 45 | stderr_logfile_maxbytes=0 -------------------------------------------------------------------------------- /deploy/docker/base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | ENV TZ=UTC 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | 6 | RUN sed -i 's|http://.*.ubuntu.com|http://mirrors.aliyun.com|g' /etc/apt/sources.list && \ 7 | sed -i 's|http://security.ubuntu.com|http://mirrors.aliyun.com|g' /etc/apt/sources.list && \ 8 | sed -i 's|http://ports.ubuntu.com|http://mirrors.aliyun.com|g' /etc/apt/sources.list 9 | 10 | RUN apt-get update && apt-get install -y --no-install-recommends \ 11 | ca-certificates \ 12 | curl \ 13 | gnupg 14 | 15 | RUN sed -i 's|http://mirrors.aliyun.com|https://mirrors.aliyun.com|g' /etc/apt/sources.list 16 | 17 | RUN apt-get update && apt-get install -y --no-install-recommends \ 18 | supervisor \ 19 | nginx \ 20 | tzdata \ 21 | vim \ 22 | python3 \ 23 | python3-pip \ 24 | python3-venv && \ 25 | ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \ 26 | echo ${TZ} > /etc/timezone && \ 27 | dpkg-reconfigure -f noninteractive tzdata 28 | 29 | RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ 30 | apt-get update && apt-get install -y nodejs && \ 31 | npm install -g npm@latest 32 | 33 | RUN python3 -m pip install --no-cache-dir uv 34 | 35 | RUN apt-get clean && rm -rf \ 36 | /var/lib/apt/lists/* \ 37 | /tmp/* /var/tmp/* \ 38 | /usr/share/doc /usr/share/man /usr/share/info /usr/share/lintian /usr/share/locale 39 | 40 | WORKDIR /app 41 | 42 | CMD ["bash"] -------------------------------------------------------------------------------- /deploy/docker/multi/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: ${POSTGRES_IMAGE:-postgres:16} 4 | environment: 5 | POSTGRES_USER: ${DB_USER:-postgres} 6 | POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} 7 | POSTGRES_DB: ${DB_NAME:-mcp_gateway} 8 | TZ: ${TZ:-UTC} 9 | volumes: 10 | - postgres_data:/var/lib/postgresql/data 11 | restart: unless-stopped 12 | 13 | web: 14 | image: ${WEB_IMAGE:-ghcr.io/mcp-ecosystem/mcp-gateway/web:latest} 15 | ports: 16 | - "80:80" 17 | - "5234:5234" 18 | environment: 19 | - ENV=production 20 | - TZ=${TZ:-UTC} 21 | volumes: 22 | - ./configs:/app/configs 23 | - ./data:/app/data 24 | - ./.env.multi:/app/.env 25 | depends_on: 26 | - postgres 27 | - mcp-gateway 28 | - mock-server 29 | restart: unless-stopped 30 | 31 | mcp-gateway: 32 | image: ${MCP_GATEWAY_IMAGE:-ghcr.io/mcp-ecosystem/mcp-gateway/mcp-gateway:latest} 33 | ports: 34 | - "5235:5235" 35 | environment: 36 | - ENV=production 37 | - TZ=${TZ:-UTC} 38 | volumes: 39 | - ./configs:/app/configs 40 | - ./data:/app/data 41 | - ./.env.multi:/app/.env 42 | depends_on: 43 | - postgres 44 | restart: unless-stopped 45 | 46 | mock-server: 47 | image: ${MOCK_USER_SVC_IMAGE:-ghcr.io/mcp-ecosystem/mcp-gateway/mock-server:latest} 48 | ports: 49 | - "5236:5236" 50 | environment: 51 | - ENV=production 52 | - TZ=${TZ:-UTC} 53 | volumes: 54 | - ./configs:/app/configs 55 | - ./data:/app/data 56 | - ./.env.multi:/app/.env 57 | depends_on: 58 | - postgres 59 | restart: unless-stopped 60 | 61 | volumes: 62 | postgres_data: 63 | -------------------------------------------------------------------------------- /deploy/docker/multi/mcp-gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.1 AS builder 2 | 3 | WORKDIR /app 4 | COPY go.mod go.sum ./ 5 | RUN go mod download 6 | COPY . . 7 | RUN GOOS=linux go build -o mcp-gateway ./cmd/mcp-gateway 8 | 9 | FROM ubuntu:22.04 10 | WORKDIR /app 11 | 12 | # Set default timezone 13 | ENV TZ=UTC 14 | 15 | RUN apt-get update && apt-get install -y \ 16 | curl \ 17 | iputils-ping \ 18 | tzdata \ 19 | && rm -rf /var/lib/apt/lists/* \ 20 | && ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime \ 21 | && echo ${TZ} > /etc/timezone \ 22 | && dpkg-reconfigure -f noninteractive tzdata 23 | 24 | COPY --from=builder /app/mcp-gateway . 25 | COPY --from=builder /app/configs/mcp-gateway.yaml /etc/mcp-gateway/ 26 | 27 | # Create data directory 28 | RUN mkdir -p /app/data 29 | 30 | EXPOSE 5235 31 | CMD ["./mcp-gateway"] -------------------------------------------------------------------------------- /deploy/docker/multi/mock-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.1 AS builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN GOOS=linux go build -o mock-server ./cmd/mock-server 5 | 6 | FROM ubuntu:22.04 7 | WORKDIR /app 8 | 9 | # Set default timezone 10 | ENV TZ=UTC 11 | 12 | RUN apt-get update && apt-get install -y \ 13 | tzdata \ 14 | && rm -rf /var/lib/apt/lists/* \ 15 | && ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime \ 16 | && echo ${TZ} > /etc/timezone \ 17 | && dpkg-reconfigure -f noninteractive tzdata 18 | 19 | COPY --from=builder /app/mock-server . 20 | 21 | EXPOSE 5236 22 | 23 | CMD ["./mock-server"] -------------------------------------------------------------------------------- /deploy/docker/multi/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | include /etc/nginx/mime.types; 7 | default_type application/octet-stream; 8 | 9 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 10 | '$status $body_bytes_sent "$http_referer" ' 11 | '"$http_user_agent" "$http_x_forwarded_for"'; 12 | access_log /var/log/nginx/access.log main; 13 | error_log /var/log/nginx/error.log; 14 | 15 | # API Server 16 | upstream apiserver { 17 | server apiserver:5234; 18 | } 19 | 20 | # MCP Gateway 21 | upstream mcp-gateway { 22 | server mcp-gateway:5236; 23 | } 24 | 25 | server { 26 | listen 80; 27 | server_name localhost; 28 | 29 | location / { 30 | root /usr/share/nginx/html; 31 | try_files $uri $uri/ /index.html; 32 | } 33 | 34 | location /api/ { 35 | proxy_pass http://apiserver/; 36 | proxy_set_header Host $host; 37 | proxy_set_header X-Real-IP $remote_addr; 38 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 39 | proxy_set_header X-Forwarded-Proto $scheme; 40 | } 41 | 42 | location /ws/ { 43 | proxy_pass http://apiserver/ws/; 44 | proxy_http_version 1.1; 45 | proxy_set_header Upgrade $http_upgrade; 46 | proxy_set_header Connection "upgrade"; 47 | proxy_set_header Host $host; 48 | proxy_set_header X-Real-IP $remote_addr; 49 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 50 | proxy_set_header X-Forwarded-Proto $scheme; 51 | } 52 | 53 | location /mcp/ { 54 | proxy_pass http://mcp-gateway; 55 | proxy_set_header Host $host; 56 | proxy_set_header X-Real-IP $remote_addr; 57 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 58 | proxy_set_header X-Forwarded-Proto $scheme; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /deploy/docker/multi/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.1 AS apiserver-builder 2 | 3 | WORKDIR /app 4 | 5 | # Copy go mod and sum files 6 | COPY go.mod go.sum ./ 7 | 8 | # Download all dependencies 9 | RUN go mod download 10 | 11 | # Copy the source code 12 | COPY . . 13 | 14 | # Build the Go service 15 | RUN GOOS=linux go build -o apiserver ./cmd/apiserver 16 | 17 | FROM node:20.18.0 AS web-builder 18 | 19 | ARG VITE_API_BASE_URL=/api 20 | ARG VITE_WS_BASE_URL=/ws 21 | ARG VITE_MCP_GATEWAY_BASE_URL=/mcp 22 | ARG VITE_BASE_URL=/ 23 | 24 | ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} \ 25 | VITE_WS_BASE_URL=${VITE_WS_BASE_URL} \ 26 | VITE_MCP_GATEWAY_BASE_URL=${VITE_MCP_GATEWAY_BASE_URL} \ 27 | VITE_BASE_URL=${VITE_BASE_URL} 28 | 29 | WORKDIR /app/web 30 | 31 | COPY web/package*.json ./ 32 | 33 | RUN npm install 34 | 35 | COPY web/ . 36 | 37 | RUN npm run build 38 | 39 | FROM nginx:1.27.5-bookworm 40 | 41 | WORKDIR /app 42 | 43 | # Set default timezone 44 | ENV TZ=UTC 45 | 46 | RUN apt-get update && apt-get install -y \ 47 | tzdata \ 48 | && rm -rf /var/lib/apt/lists/* \ 49 | && ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime \ 50 | && echo ${TZ} > /etc/timezone \ 51 | && dpkg-reconfigure -f noninteractive tzdata 52 | 53 | # Copy apiserver binary and config 54 | COPY --from=apiserver-builder /app/apiserver /app/ 55 | COPY --from=apiserver-builder /app/configs /app/configs 56 | COPY --from=apiserver-builder /app/configs/apiserver.yaml /etc/mcp-gateway/ 57 | 58 | # Create data directory for apiserver with proper permissions 59 | RUN mkdir -p /app/data && \ 60 | chmod -R 777 /app/data && \ 61 | mkdir -p /tmp/mcp-gateway && \ 62 | chmod -R 777 /tmp/mcp-gateway 63 | 64 | # Set minimum required environment variables 65 | ENV APISERVER_I18N_PATH=/app/configs/i18n \ 66 | GATEWAY_DB_NAME=/app/data/mcp-gateway.db 67 | 68 | # Copy web files to nginx html directory 69 | COPY --from=web-builder /app/web/dist /usr/share/nginx/html 70 | 71 | # Copy nginx configuration 72 | COPY deploy/docker/multi/web/nginx.conf /etc/nginx/nginx.conf 73 | 74 | # Expose ports 75 | EXPOSE 80 5234 76 | 77 | # Create startup script 78 | RUN echo '#!/bin/bash\n\ 79 | nginx &\n\ 80 | exec /app/apiserver\n' > /app/start.sh && \ 81 | chmod +x /app/start.sh 82 | 83 | CMD ["/app/start.sh"] -------------------------------------------------------------------------------- /deploy/docker/multi/web/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | include /etc/nginx/mime.types; 7 | default_type application/octet-stream; 8 | 9 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 10 | '$status $body_bytes_sent "$http_referer" ' 11 | '"$http_user_agent" "$http_x_forwarded_for"'; 12 | access_log /dev/stdout main; 13 | error_log /dev/stderr; 14 | 15 | # API Server (running locally in the same container) 16 | upstream apiserver { 17 | server localhost:5234; 18 | } 19 | 20 | server { 21 | listen 80; 22 | server_name localhost; 23 | 24 | location / { 25 | root /usr/share/nginx/html; 26 | try_files $uri $uri/ /index.html; 27 | } 28 | 29 | location /api/ { 30 | proxy_pass http://apiserver/api/; 31 | proxy_set_header Host $host; 32 | proxy_set_header X-Real-IP $remote_addr; 33 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 34 | proxy_set_header X-Forwarded-Proto $scheme; 35 | } 36 | 37 | location /ws/ { 38 | proxy_pass http://apiserver/api/ws/; 39 | proxy_http_version 1.1; 40 | proxy_set_header Upgrade $http_upgrade; 41 | proxy_set_header Connection "upgrade"; 42 | proxy_set_header Host $host; 43 | proxy_set_header X-Real-IP $remote_addr; 44 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 45 | proxy_set_header X-Forwarded-Proto $scheme; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /deploy/k8s/multi/mcp-gateway-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: mcp-gateway 5 | namespace: mcp-gateway 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: mcp-gateway 11 | template: 12 | metadata: 13 | labels: 14 | app: mcp-gateway 15 | spec: 16 | containers: 17 | - name: mcp-gateway 18 | imagePullPolicy: IfNotPresent # 优先使用本地镜像 19 | image: ghcr.io/mcp-ecosystem/mcp-gateway/mcp-gateway:latest 20 | env: 21 | - name: ENV 22 | value: production 23 | - name: TZ 24 | value: UTC 25 | envFrom: 26 | - configMapRef: 27 | name: app-env 28 | ports: 29 | - containerPort: 5235 30 | volumeMounts: 31 | - name: configs 32 | mountPath: /app/configs 33 | - name: data 34 | mountPath: /app/data 35 | volumes: 36 | - name: configs 37 | configMap: 38 | name: app-configs 39 | - name: data 40 | emptyDir: {} 41 | --- 42 | apiVersion: v1 43 | kind: Service 44 | metadata: 45 | name: mcp-gateway 46 | namespace: mcp-gateway 47 | spec: 48 | type: NodePort 49 | ports: 50 | - port: 5235 51 | targetPort: 5235 52 | nodePort: 30235 53 | name: mcp-gateway 54 | - port: 5245 55 | targetPort: 5245 56 | nodePort: 30245 57 | name: mcp-gateway-notifier 58 | selector: 59 | app: mcp-gateway -------------------------------------------------------------------------------- /deploy/k8s/multi/mock-user-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: mock-user 5 | namespace: mcp-gateway 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: mock-user 11 | template: 12 | metadata: 13 | labels: 14 | app: mock-user 15 | spec: 16 | containers: 17 | - name: mock-user 18 | imagePullPolicy: IfNotPresent # 优先使用本地镜像 19 | image: ghcr.io/mcp-ecosystem/mcp-gateway/mock-user:latest 20 | env: 21 | - name: ENV 22 | value: production 23 | - name: TZ 24 | value: UTC 25 | ports: 26 | - containerPort: 5236 27 | volumeMounts: 28 | - name: configs 29 | mountPath: /app/configs 30 | - name: data 31 | mountPath: /app/data 32 | - name: env-file 33 | mountPath: /app/.env 34 | subPath: .env 35 | volumes: 36 | - name: configs 37 | configMap: 38 | name: app-configs 39 | - name: data 40 | emptyDir: {} 41 | - name: env-file 42 | configMap: 43 | name: app-env 44 | --- 45 | apiVersion: v1 46 | kind: Service 47 | metadata: 48 | name: mock-user 49 | namespace: mcp-gateway 50 | spec: 51 | type: NodePort 52 | ports: 53 | - port: 5236 54 | targetPort: 5236 55 | nodePort: 30236 56 | selector: 57 | app: mock-user -------------------------------------------------------------------------------- /deploy/k8s/multi/postgres-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: postgres 5 | namespace: mcp-gateway 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: postgres 11 | template: 12 | metadata: 13 | labels: 14 | app: postgres 15 | spec: 16 | containers: 17 | - name: postgres 18 | image: postgres:16 19 | env: 20 | - name: POSTGRES_USER 21 | value: postgres 22 | - name: POSTGRES_PASSWORD 23 | value: postgres 24 | - name: POSTGRES_DB 25 | value: mcp_gateway 26 | - name: TZ 27 | value: UTC 28 | ports: 29 | - containerPort: 5432 30 | volumeMounts: 31 | - name: postgres-data 32 | mountPath: /var/lib/postgresql/data 33 | volumes: 34 | - name: postgres-data 35 | emptyDir: {} # 使用 emptyDir 替代 PVC,但数据会在 Pod 重启后丢失 36 | --- 37 | apiVersion: v1 38 | kind: Service 39 | metadata: 40 | name: postgres 41 | namespace: mcp-gateway 42 | spec: 43 | type: NodePort 44 | ports: 45 | - port: 5432 46 | targetPort: 5432 47 | nodePort: 30432 # 指定一个 30000-32767 之间的端口 48 | selector: 49 | app: postgres -------------------------------------------------------------------------------- /deploy/k8s/multi/web-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: web 5 | namespace: mcp-gateway 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: web 11 | template: 12 | metadata: 13 | labels: 14 | app: web 15 | spec: 16 | containers: 17 | - name: web 18 | imagePullPolicy: IfNotPresent # 优先使用本地镜像 19 | image: ghcr.io/mcp-ecosystem/mcp-gateway/web:latest 20 | env: 21 | - name: TZ 22 | value: UTC 23 | - name: ENV 24 | value: production 25 | envFrom: 26 | - configMapRef: 27 | name: app-env 28 | ports: 29 | - containerPort: 80 30 | - containerPort: 5234 31 | volumeMounts: 32 | - name: configs 33 | mountPath: /app/configs 34 | - name: i18n-files 35 | mountPath: /app/configs/i18n 36 | - name: data 37 | mountPath: /app/data 38 | # - name: web-env 39 | # mountPath: /app/.env 40 | # subPath: .env 41 | volumes: 42 | - name: configs 43 | configMap: 44 | name: app-configs 45 | - name: data 46 | emptyDir: {} 47 | # - name: web-env 48 | # configMap: 49 | # name: web-env 50 | - name: i18n-files 51 | configMap: 52 | name: i18n-config 53 | --- 54 | apiVersion: v1 55 | kind: Service 56 | metadata: 57 | name: web 58 | namespace: mcp-gateway 59 | spec: 60 | type: NodePort 61 | ports: 62 | - port: 80 63 | targetPort: 80 64 | nodePort: 30080 65 | name: my-web 66 | - port: 5234 67 | targetPort: 5234 68 | nodePort: 30234 69 | name: my-web-api 70 | selector: 71 | app: web -------------------------------------------------------------------------------- /docs/.ai/SOP.client.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Standard Operating Procedures (SOP) Manual 2 | 3 | ## 代码生成原则 4 | 5 | ### 1. 代码可读性 6 | - 编写干净、结构清晰的代码 7 | - 使用有意义的变量名和函数名 8 | - 保持一致的格式和缩进风格 9 | 10 | ### 2. 文档说明 11 | - 注释必须: 12 | - **仅使用英文编写** 13 | - **仅在必要时添加** 14 | - **用于解释代码的意图、复杂逻辑或不易理解的部分** 15 | 16 | ### 3. 开发流程 17 | - 在编写代码之前先认真思考问题 18 | - 从第一性原理和基本需求出发进行分析 19 | - 预先规划代码结构和整体架构 20 | - 将复杂问题拆分为更小、更易处理的部分 21 | - **优先确认是否已经存在基础建设或相关代码,仿造现有风格胜过从0开始** 22 | 23 | ### 4. 最佳实践 24 | - 遵循特定编程语言的约定和风格指南 25 | - 在适当的位置实现错误处理机制 26 | - 编写可维护、可扩展的代码 27 | - 考虑性能影响 28 | 29 | ## 项目相关 30 | 31 | - 到服务端的路由都配置在了vite里,部分是通过.env注入的 32 | - 主要的后端是 `apiserver` 33 | 34 | ### 5. 国际化 (i18n) 规范 35 | - 所有显示给用户的文本内容必须使用i18n国际化处理 36 | - 翻译文件位于 `web/src/i18n/locales` 目录下 37 | - 英文翻译: `web/src/i18n/locales/en/translation.json` 38 | - 中文翻译: `web/src/i18n/locales/zh/translation.json` 39 | - 添加新文本时必须同时更新中英文两种语言的翻译文件 40 | - 使用时通过 `t()` 函数引用翻译键值,例如: `t('common.save')` 41 | 42 | -------------------------------------------------------------------------------- /docs/.ai/SOP.server.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Standard Operating Procedures (SOP) Manual 2 | 3 | ## 代码生成原则 4 | 5 | ### 1. 代码可读性 6 | - 编写干净、结构清晰的代码 7 | - 使用有意义的变量名和函数名 8 | - 保持一致的格式和缩进风格 9 | 10 | ### 2. 文档说明 11 | - 注释必须: 12 | - **仅使用英文编写** 13 | - **仅在必要时添加** 14 | - **用于解释代码的意图、复杂逻辑或不易理解的部分** 15 | 16 | ### 3. 开发流程 17 | - 在编写代码之前先认真思考问题 18 | - 从第一性原理和基本需求出发进行分析 19 | - 预先规划代码结构和整体架构 20 | - 将复杂问题拆分为更小、更易处理的部分 21 | - **优先确认是否已经存在基础建设或相关代码,仿造现有风格胜过从0开始** 22 | 23 | ### 4. 最佳实践 24 | - 遵循特定编程语言的约定和风格指南 25 | - 在适当的位置实现错误处理机制 26 | - 编写可维护、可扩展的代码 27 | - 考虑性能影响 28 | 29 | ## 项目相关 30 | 31 | web对应的后端服务是 `cmd/apiserver` ,而 `cmd/mcp-gateway` 是网关服务,可以理解是前者是控制面,后者是数据面。所以你需要先判断需求是要修改哪个服务 32 | 33 | -------------------------------------------------------------------------------- /docs/.ai/release.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | 每次发布新版本时,按照以下进行操作: 4 | 1. 可以通过`cat pkg/version/VERSION` 5 | 2. 请参考changelog/`cat pkg/version/VERSION`.md文件的书写风格和格式(例如标准开头,然后先中文后英文) 6 | 3. 通过git命令去查找从前一个版本到现在的变更,如`git log v0.2.6..HEAD --pretty=format:"%h %s" | cat` 7 | 4. 改变pkg/version/VERSION和web/package.json(更新后最好cd web && npm i一下)中的版本号,如果没有特别指明,通常是+0.0.1 8 | 5. 根据2和3的内容,新增新版本的变更内容到changelog/`cat pkg/version/VERSION`.md -------------------------------------------------------------------------------- /docs/i18n_summary.md: -------------------------------------------------------------------------------- 1 | # 国际化错误处理总结 2 | 3 | 我们已经实现了一个新的国际化错误处理系统,它允许直接在错误发生点创建和翻译错误消息,而不需要通过中间件解析和修改响应体。 4 | 5 | ## 主要组件 6 | 7 | 1. **I18nError类型** - 定义在`pkg/i18n/error.go`中,包含消息ID、默认消息和模板数据 8 | 2. **ErrorWithCode类型** - 扩展I18nError,添加了HTTP状态码支持 9 | 3. **全局翻译器** - 在应用程序启动时初始化,用于所有国际化消息的翻译 10 | 4. **辅助函数** - 简化错误创建和处理的函数,如`RespondWithError`和`TranslateMessageGin` 11 | 12 | ## 用法示例 13 | 14 | ### 创建国际化错误 15 | 16 | ```go 17 | // 使用预定义错误 18 | return i18n.ErrNotFound.WithParam("ID", id) 19 | 20 | // 创建自定义错误 21 | return i18n.NewErrorWithCode("ErrorTenantNotFound", i18n.ErrorNotFound).WithParam("Name", tenantName) 22 | ``` 23 | 24 | ### 在HTTP处理程序中使用 25 | 26 | ```go 27 | func GetResource(c *gin.Context) { 28 | id := c.Param("id") 29 | resource, err := resourceService.GetByID(id) 30 | if err != nil { 31 | // 简单方式:使用辅助函数发送错误响应 32 | i18n.RespondWithError(c, i18n.ErrNotFound.WithParam("ID", id)) 33 | return 34 | } 35 | 36 | // 翻译成功消息 37 | c.JSON(http.StatusOK, gin.H{ 38 | "message": i18n.TranslateMessageGin("SuccessResourceFound", c, nil), 39 | "data": resource, 40 | }) 41 | } 42 | ``` 43 | 44 | ## 工作原理 45 | 46 | 1. 当一个请求到达服务器时,`I18nMiddleware`中间件会提取并存储用户的语言首选项 47 | 2. 当需要返回一个错误时,直接创建一个`I18nError`或使用预定义错误 48 | 3. 使用`RespondWithError`发送带有适当状态码和翻译错误消息的HTTP响应 49 | 4. 使用`TranslateMessageGin`翻译成功消息或其他消息字符串 50 | 51 | ## 优势 52 | 53 | 1. **简单直接** - 不需要复杂的中间件逻辑来解析和修改响应 54 | 2. **更好的类型安全** - 使用专用错误类型而不是字符串 55 | 3. **更好的语义** - 错误定义在它们发生的地方 56 | 4. **一致的接口** - 辅助函数提供一致的错误处理接口 57 | 58 | ## 预定义错误 59 | 60 | ```go 61 | var ( 62 | ErrNotFound = NewErrorWithCode("ErrorResourceNotFound", ErrorNotFound) 63 | ErrUnauthorized = NewErrorWithCode("ErrorUnauthorized", ErrorUnauthorized) 64 | ErrForbidden = NewErrorWithCode("ErrorForbidden", ErrorForbidden) 65 | ErrBadRequest = NewErrorWithCode("ErrorBadRequest", ErrorBadRequest) 66 | ErrInternalServer = NewErrorWithCode("ErrorInternalServer", ErrorInternalServer) 67 | ) 68 | ``` 69 | 70 | ## 翻译文件 71 | 72 | 翻译文件位于`configs/i18n/{lang}/messages.toml`,使用以下格式: 73 | 74 | ```toml 75 | [ErrorTenantNotFound] 76 | other = "Tenant with name '{{.Name}}' not found" 77 | ``` 78 | 79 | ## 进一步改进 80 | 81 | 1. 添加更多预定义错误 82 | 2. 为特定模块创建专用错误 83 | 3. 添加日志记录和错误追踪功能 84 | 4. 考虑添加错误分类和分组支持 -------------------------------------------------------------------------------- /docs/specs/MCP-Error-Handling.md: -------------------------------------------------------------------------------- 1 | # MCP 错误响应规范(针对 SSE 和 Streamable HTTP) 2 | 3 | ## 通用要求 4 | 5 | - **HTTP状态码**用于表示请求是否被接受。 6 | - **JSON-RPC 错误对象**用于描述具体错误详情。 7 | 8 | 标准 JSON-RPC 错误对象格式: 9 | 10 | ```json 11 | { 12 | "jsonrpc": "2.0", 13 | "id": "与请求对应的ID或null", 14 | "error": { 15 | "code": 错误码, 16 | "message": "错误简要描述", 17 | "data": 可选的附加信息 18 | } 19 | } 20 | ``` 21 | 22 | 常用错误码(遵循 JSON-RPC): 23 | 24 | | 错误码 | 含义 | 25 | |-------|----------------| 26 | | -32700 | 解析错误 | 27 | | -32600 | 无效请求 | 28 | | -32601 | 方法未找到 | 29 | | -32602 | 无效参数 | 30 | | -32603 | 内部错误 | 31 | | -32000~-32099 | 服务器自定义错误 | 32 | 33 | --- 34 | 35 | ## SSE 模式下错误处理 36 | 37 | - **请求格式错误** ➔ `400 Bad Request`,可附错误JSON体,无`id`。 38 | - **仅发送通知** ➔ `202 Accepted`,无正文。 39 | - **正常请求** ➔ `200 OK`,开启 `Content-Type: text/event-stream` 流。 40 | - 每个请求对应至少一个响应(成功或错误)。 41 | - 错误通过 SSE `data:` 发送标准 JSON-RPC 错误对象。 42 | 43 | 示例 SSE 错误事件: 44 | 45 | ```text 46 | data: {"jsonrpc":"2.0","id":"123","error":{"code":-32601,"message":"Method not found"}} 47 | ``` 48 | 49 | - **SSE连接不支持** ➔ `405 Method Not Allowed`。 50 | 51 | --- 52 | 53 | ## Streamable HTTP 模式下错误处理 54 | 55 | - **请求格式错误** ➔ `400 Bad Request`。 56 | - **仅发送通知** ➔ `202 Accepted`。 57 | - **成功请求返回** ➔ `200 OK`,直接返回JSON对象或数组。 58 | - 单个请求出错 ➔ 响应体为错误对象。 59 | - 批量请求部分出错 ➔ 响应体是数组,出错条目含`error`字段。 60 | - **Accept: text/event-stream** ➔ 返回 SSE 流,与 SSE 模式规则一致。 61 | 62 | --- 63 | 64 | ## 小结 65 | 66 | - HTTP层状态码表示请求是否被接受。 67 | - JSON-RPC层错误对象提供具体错误信息。 68 | - 每个请求,无论成功或失败,都必须有对应响应。 69 | 70 | ## 参考 71 | - https://modelcontextprotocol.io/specification/2025-03-26/server/tools#error-handling 72 | - https://modelcontextprotocol.io/docs/concepts/architecture#error-handling 73 | -------------------------------------------------------------------------------- /internal/apiserver/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Database defines the methods for database operations. 8 | type Database interface { 9 | // Close closes the database connection. 10 | Close() error 11 | 12 | // SaveMessage saves a message to the database. 13 | SaveMessage(ctx context.Context, message *Message) error 14 | // GetMessages gets messages for a specific session. 15 | GetMessages(ctx context.Context, sessionID string) ([]*Message, error) 16 | // GetMessagesWithPagination gets messages for a specific session with pagination. 17 | GetMessagesWithPagination(ctx context.Context, sessionID string, page, pageSize int) ([]*Message, error) 18 | // CreateSession creates a new session with the given sessionId. 19 | CreateSession(ctx context.Context, sessionId string) error 20 | // SessionExists checks if a session exists. 21 | SessionExists(ctx context.Context, sessionID string) (bool, error) 22 | // GetSessions gets all chat sessions with their latest message. 23 | GetSessions(ctx context.Context) ([]*Session, error) 24 | // UpdateSessionTitle updates the title of a session. 25 | UpdateSessionTitle(ctx context.Context, sessionID string, title string) error 26 | // DeleteSession deletes a session by ID. 27 | DeleteSession(ctx context.Context, sessionID string) error 28 | 29 | CreateUser(ctx context.Context, user *User) error 30 | GetUserByUsername(ctx context.Context, username string) (*User, error) 31 | UpdateUser(ctx context.Context, user *User) error 32 | DeleteUser(ctx context.Context, id uint) error 33 | ListUsers(ctx context.Context) ([]*User, error) 34 | 35 | CreateTenant(ctx context.Context, tenant *Tenant) error 36 | GetTenantByName(ctx context.Context, name string) (*Tenant, error) 37 | GetTenantByID(ctx context.Context, id uint) (*Tenant, error) 38 | UpdateTenant(ctx context.Context, tenant *Tenant) error 39 | DeleteTenant(ctx context.Context, id uint) error 40 | ListTenants(ctx context.Context) ([]*Tenant, error) 41 | 42 | AddUserToTenant(ctx context.Context, userID, tenantID uint) error 43 | RemoveUserFromTenant(ctx context.Context, userID, tenantID uint) error 44 | GetUserTenants(ctx context.Context, userID uint) ([]*Tenant, error) 45 | GetTenantUsers(ctx context.Context, tenantID uint) ([]*User, error) 46 | DeleteUserTenants(ctx context.Context, userID uint) error 47 | 48 | Transaction(ctx context.Context, fn func(ctx context.Context) error) error 49 | } 50 | -------------------------------------------------------------------------------- /internal/apiserver/database/factory.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mcp-ecosystem/mcp-gateway/internal/common/config" 7 | ) 8 | 9 | // NewDatabase creates a new database based on configuration 10 | func NewDatabase(cfg *config.DatabaseConfig) (Database, error) { 11 | switch cfg.Type { 12 | case "postgres": 13 | return NewPostgres(cfg) 14 | case "sqlite": 15 | return NewSQLite(cfg) 16 | case "mysql": 17 | return NewMySQL(cfg) 18 | default: 19 | return nil, fmt.Errorf("unsupported database type: %s", cfg.Type) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/apiserver/database/model.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "time" 4 | 5 | // Session represents a chat session 6 | type Session struct { 7 | ID string `json:"id"` 8 | CreatedAt time.Time `json:"createdAt"` 9 | Title string `json:"title"` 10 | } 11 | 12 | // Message represents a chat message 13 | type Message struct { 14 | ID string `json:"id"` 15 | SessionID string `json:"session_id"` 16 | Content string `json:"content"` 17 | Sender string `json:"sender"` 18 | Timestamp time.Time `json:"timestamp"` 19 | ToolCalls string `json:"toolCalls,omitempty"` 20 | ToolResult string `json:"toolResult,omitempty"` 21 | } 22 | 23 | // UserRole represents the role of a user 24 | type UserRole string 25 | 26 | const ( 27 | RoleAdmin UserRole = "admin" 28 | RoleNormal UserRole = "normal" 29 | ) 30 | 31 | // User represents an admin user 32 | type User struct { 33 | ID uint `json:"id" gorm:"primaryKey;autoIncrement"` 34 | Username string `json:"username" gorm:"type:varchar(50);uniqueIndex"` 35 | Password string `json:"-" gorm:"not null"` // Password is not exposed in JSON 36 | Role UserRole `json:"role" gorm:"not null;default:'normal'"` 37 | IsActive bool `json:"isActive" gorm:"not null;default:true"` 38 | CreatedAt time.Time `json:"createdAt"` 39 | UpdatedAt time.Time `json:"updatedAt"` 40 | } 41 | 42 | // Tenant represents a tenant in the system 43 | type Tenant struct { 44 | ID uint `json:"id" gorm:"primaryKey;autoIncrement"` 45 | Name string `json:"name" gorm:"type:varchar(50);uniqueIndex"` 46 | Prefix string `json:"prefix" gorm:"type:varchar(50);uniqueIndex"` 47 | Description string `json:"description" gorm:"type:varchar(255)"` 48 | IsActive bool `json:"isActive" gorm:"not null;default:true"` 49 | CreatedAt time.Time `json:"createdAt"` 50 | UpdatedAt time.Time `json:"updatedAt"` 51 | } 52 | 53 | // UserTenant represents the relationship between a user and a tenant 54 | type UserTenant struct { 55 | ID uint `json:"id" gorm:"primaryKey;autoIncrement"` 56 | UserID uint `json:"userId" gorm:"index:idx_user_tenant,unique;not null"` 57 | TenantID uint `json:"tenantId" gorm:"index:idx_user_tenant,unique;not null"` 58 | CreatedAt time.Time `json:"createdAt"` 59 | UpdatedAt time.Time `json:"updatedAt"` 60 | } 61 | -------------------------------------------------------------------------------- /internal/apiserver/database/tx.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | // txKey is the context key used to store transactions 10 | type txKey struct{} 11 | 12 | // TransactionFromContext extracts a transaction from the context 13 | func TransactionFromContext(ctx context.Context) *gorm.DB { 14 | tx, ok := ctx.Value(txKey{}).(*gorm.DB) 15 | if !ok { 16 | return nil 17 | } 18 | return tx 19 | } 20 | 21 | // ContextWithTransaction creates a context containing a transaction 22 | func ContextWithTransaction(ctx context.Context, tx *gorm.DB) context.Context { 23 | return context.WithValue(ctx, txKey{}, tx) 24 | } 25 | 26 | // getDBFromContext gets the DB object, using the transaction from context if available 27 | func getDBFromContext(ctx context.Context, db *gorm.DB) *gorm.DB { 28 | tx := TransactionFromContext(ctx) 29 | if tx != nil { 30 | return tx 31 | } 32 | return db.WithContext(ctx) 33 | } 34 | -------------------------------------------------------------------------------- /internal/apiserver/database/util.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "gorm.io/gorm" 9 | ) 10 | 11 | // InitDefaultTenant initializes the default tenant if it doesn't exist 12 | func InitDefaultTenant(db *gorm.DB) error { 13 | ctx := context.Background() 14 | 15 | // Check if default tenant already exists 16 | var count int64 17 | if err := db.Model(&Tenant{}).Where("name = ?", "default").Count(&count).Error; err != nil { 18 | return err 19 | } 20 | 21 | if count > 0 { 22 | // Default tenant already exists 23 | return nil 24 | } 25 | 26 | // Create default tenant 27 | defaultTenant := &Tenant{ 28 | Name: "default", 29 | Prefix: "/mcp", 30 | Description: "Default tenant for MCP Gateway", 31 | IsActive: true, 32 | CreatedAt: time.Now(), 33 | UpdatedAt: time.Now(), 34 | } 35 | 36 | if err := db.WithContext(ctx).Create(defaultTenant).Error; err != nil { 37 | return err 38 | } 39 | 40 | // Find all admin users 41 | var adminUsers []*User 42 | if err := db.WithContext(ctx).Where("role = ?", RoleAdmin).Find(&adminUsers).Error; err != nil { 43 | return err 44 | } 45 | 46 | // Grant admin users access to the default tenant 47 | for _, user := range adminUsers { 48 | userTenant := &UserTenant{ 49 | UserID: user.ID, 50 | TenantID: defaultTenant.ID, 51 | CreatedAt: time.Now(), 52 | UpdatedAt: time.Now(), 53 | } 54 | 55 | // Ignore duplicate key errors 56 | if err := db.WithContext(ctx).Create(userTenant).Error; err != nil { 57 | if !strings.Contains(err.Error(), "duplicate key") && !strings.Contains(err.Error(), "unique constraint") { 58 | return err 59 | } 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/apiserver/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/mcp-ecosystem/mcp-gateway/internal/auth/jwt" 9 | ) 10 | 11 | // JWTAuthMiddleware creates a middleware that validates JWT tokens 12 | func JWTAuthMiddleware(jwtService *jwt.Service) gin.HandlerFunc { 13 | return func(c *gin.Context) { 14 | // Get the Authorization header 15 | authHeader := c.GetHeader("Authorization") 16 | if authHeader == "" { 17 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) 18 | return 19 | } 20 | 21 | // Check if the header has the Bearer prefix 22 | parts := strings.Split(authHeader, " ") 23 | if len(parts) != 2 || parts[0] != "Bearer" { 24 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) 25 | return 26 | } 27 | 28 | // Validate the token 29 | claims, err := jwtService.ValidateToken(parts[1]) 30 | if err != nil { 31 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) 32 | return 33 | } 34 | 35 | // Add the claims to the context 36 | c.Set("claims", claims) 37 | c.Next() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/auth/authenticator.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/mcp-ecosystem/mcp-gateway/internal/auth/impl" 8 | "github.com/mcp-ecosystem/mcp-gateway/internal/auth/types" 9 | ) 10 | 11 | // Authenticator defines the interface for authentication 12 | type Authenticator interface { 13 | // Authenticate authenticates the request 14 | Authenticate(ctx context.Context, r *http.Request) error 15 | } 16 | 17 | // Mode represents the authentication mode 18 | type Mode string 19 | 20 | const ( 21 | // ModeNone represents no authentication 22 | ModeNone Mode = "none" 23 | // ModeBearer represents bearer token authentication 24 | ModeBearer Mode = "bearer" 25 | // ModeAPIKey represents API key authentication 26 | ModeAPIKey Mode = "apikey" 27 | ) 28 | 29 | // NewAuthenticator creates a new authenticator based on the mode 30 | func NewAuthenticator(mode types.Mode, header, argKey string) types.Authenticator { 31 | switch mode { 32 | case types.ModeBearer: 33 | return &impl.BearerAuthenticator{ 34 | Header: header, 35 | ArgKey: argKey, 36 | } 37 | case types.ModeAPIKey: 38 | return &impl.APIKeyAuthenticator{ 39 | Header: header, 40 | ArgKey: argKey, 41 | } 42 | case types.ModeNone: 43 | return &impl.NoopAuthenticator{ 44 | Header: header, 45 | ArgKey: argKey, 46 | } 47 | default: 48 | return &impl.NoopAuthenticator{ 49 | Header: header, 50 | ArgKey: argKey, 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/auth/impl/apikey.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // APIKeyAuthenticator implements API key authentication 10 | type APIKeyAuthenticator struct { 11 | Header string 12 | ArgKey string 13 | } 14 | 15 | // Authenticate implements types.Authenticator.Authenticate 16 | func (a *APIKeyAuthenticator) Authenticate(ctx context.Context, r *http.Request) error { 17 | // Get API key from header 18 | apiKey := r.Header.Get(a.Header) 19 | if apiKey == "" { 20 | return fmt.Errorf("missing %s header", a.Header) 21 | } 22 | 23 | // Store API key in context for later use 24 | ctx = context.WithValue(ctx, a.ArgKey, apiKey) 25 | *r = *r.WithContext(ctx) 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/auth/impl/basic.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/mcp-ecosystem/mcp-gateway/internal/auth/types" 11 | ) 12 | 13 | // BasicAuthenticator implements Basic authentication 14 | type BasicAuthenticator struct { 15 | Realm string 16 | } 17 | 18 | // Authenticate implements types.Authenticator.Authenticate 19 | func (a *BasicAuthenticator) Authenticate(ctx context.Context, r *http.Request) error { 20 | auth := r.Header.Get("Authorization") 21 | if auth == "" { 22 | return fmt.Errorf("missing Authorization header") 23 | } 24 | 25 | if !strings.HasPrefix(auth, "Basic ") { 26 | return fmt.Errorf("invalid Authorization header format") 27 | } 28 | 29 | // Decode credentials 30 | credentials, err := base64.StdEncoding.DecodeString(auth[6:]) 31 | if err != nil { 32 | return fmt.Errorf("invalid base64 encoding: %v", err) 33 | } 34 | 35 | // Split username and password 36 | parts := strings.SplitN(string(credentials), ":", 2) 37 | if len(parts) != 2 { 38 | return fmt.Errorf("invalid credentials format") 39 | } 40 | 41 | username, password := parts[0], parts[1] 42 | 43 | // Store credentials in context for later use 44 | ctx = context.WithValue(ctx, types.ContextKeyUsername, username) 45 | ctx = context.WithValue(ctx, types.ContextKeyPassword, password) 46 | *r = *r.WithContext(ctx) 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/auth/impl/bearer.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | // BearerAuthenticator implements bearer token authentication 11 | type BearerAuthenticator struct { 12 | Header string 13 | ArgKey string 14 | } 15 | 16 | // Authenticate implements types.Authenticator.Authenticate 17 | func (a *BearerAuthenticator) Authenticate(ctx context.Context, r *http.Request) error { 18 | // Get token from header 19 | token := r.Header.Get(a.Header) 20 | if token == "" { 21 | return fmt.Errorf("missing %s header", a.Header) 22 | } 23 | 24 | // Validate token format (Bearer ) 25 | parts := strings.SplitN(token, " ", 2) 26 | if len(parts) != 2 || parts[0] != "Bearer" { 27 | return fmt.Errorf("invalid token format") 28 | } 29 | 30 | // Store token in context for later use 31 | ctx = context.WithValue(ctx, a.ArgKey, parts[1]) 32 | *r = *r.WithContext(ctx) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/auth/impl/noop.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // NoopAuthenticator implements no-op authentication 9 | type NoopAuthenticator struct { 10 | Header string 11 | ArgKey string 12 | } 13 | 14 | // Authenticate implements Authenticator.Authenticate 15 | func (a *NoopAuthenticator) Authenticate(ctx context.Context, r *http.Request) error { 16 | // No-op authentication 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /internal/auth/impl/oauth2.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/mcp-ecosystem/mcp-gateway/internal/auth/oauth2" 11 | ) 12 | 13 | var ( 14 | // ErrUnauthorized is returned when authentication fails 15 | ErrUnauthorized = errors.New("unauthorized") 16 | ) 17 | 18 | // OAuth2 implements the auth.Authenticator interface using OAuth2 19 | type OAuth2 struct { 20 | client oauth2.Client 21 | } 22 | 23 | // NewOAuth2 creates a new OAuth2 authenticator 24 | func NewOAuth2(client oauth2.Client) *OAuth2 { 25 | return &OAuth2{ 26 | client: client, 27 | } 28 | } 29 | 30 | // Authenticate implements the auth.Authenticator interface 31 | func (o *OAuth2) Authenticate(ctx context.Context, r *http.Request) (context.Context, error) { 32 | // Get token from Authorization header 33 | authHeader := r.Header.Get("Authorization") 34 | if authHeader == "" { 35 | return ctx, ErrUnauthorized 36 | } 37 | 38 | // Check if it's a Bearer token 39 | parts := strings.Split(authHeader, " ") 40 | if len(parts) != 2 || parts[0] != "Bearer" { 41 | return ctx, ErrUnauthorized 42 | } 43 | 44 | token := parts[1] 45 | if token == "" { 46 | return ctx, ErrUnauthorized 47 | } 48 | 49 | // Validate token and get claims 50 | claims, err := o.client.ValidateToken(ctx, token) 51 | if err != nil { 52 | return ctx, fmt.Errorf("validate token: %w", err) 53 | } 54 | 55 | // Store claims in context 56 | return oauth2.WithClaims(ctx, claims), nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/auth/impl/oauth2_test.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/mcp-ecosystem/mcp-gateway/internal/auth/oauth2" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | type mockOAuth2Client struct { 14 | mock.Mock 15 | } 16 | 17 | func (m *mockOAuth2Client) ValidateToken(ctx context.Context, token string) (oauth2.Claims, error) { 18 | args := m.Called(ctx, token) 19 | return args.Get(0).(oauth2.Claims), args.Error(1) 20 | } 21 | 22 | func TestOAuth2_Authenticate(t *testing.T) { 23 | tests := []struct { 24 | name string 25 | authHeader string 26 | token string 27 | claims oauth2.Claims 28 | validateError error 29 | wantError bool 30 | }{ 31 | { 32 | name: "valid token", 33 | authHeader: "Bearer valid-token", 34 | token: "valid-token", 35 | claims: oauth2.Claims{"sub": "user123"}, 36 | wantError: false, 37 | }, 38 | { 39 | name: "missing authorization header", 40 | authHeader: "", 41 | wantError: true, 42 | }, 43 | { 44 | name: "invalid authorization header format", 45 | authHeader: "InvalidFormat", 46 | wantError: true, 47 | }, 48 | { 49 | name: "empty token", 50 | authHeader: "Bearer ", 51 | wantError: true, 52 | }, 53 | { 54 | name: "invalid token", 55 | authHeader: "Bearer invalid-token", 56 | token: "invalid-token", 57 | validateError: assert.AnError, 58 | wantError: true, 59 | }, 60 | } 61 | 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | // Create mock client 65 | mockClient := new(mockOAuth2Client) 66 | if tt.token != "" { 67 | mockClient.On("ValidateToken", mock.Anything, tt.token).Return(tt.claims, tt.validateError) 68 | } 69 | 70 | // Create authenticator 71 | authenticator := NewOAuth2(mockClient) 72 | 73 | // Create request 74 | req, err := http.NewRequest("GET", "/", nil) 75 | assert.NoError(t, err) 76 | if tt.authHeader != "" { 77 | req.Header.Set("Authorization", tt.authHeader) 78 | } 79 | 80 | // Authenticate 81 | ctx, err := authenticator.Authenticate(context.Background(), req) 82 | 83 | // Check error 84 | if tt.wantError { 85 | assert.Error(t, err) 86 | } else { 87 | assert.NoError(t, err) 88 | // Check claims in context 89 | claims, _ := oauth2.GetClaims(ctx) 90 | assert.Equal(t, tt.claims, claims) 91 | } 92 | 93 | // Verify mock expectations 94 | mockClient.AssertExpectations(t) 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /internal/auth/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/golang-jwt/jwt/v5" 8 | ) 9 | 10 | var ( 11 | ErrInvalidToken = errors.New("invalid token") 12 | ErrExpiredToken = errors.New("token has expired") 13 | ) 14 | 15 | // Claims represents the JWT claims 16 | type Claims struct { 17 | UserID uint `json:"user_id"` 18 | Username string `json:"username"` 19 | Role string `json:"role"` 20 | jwt.RegisteredClaims 21 | } 22 | 23 | // Config represents the JWT configuration 24 | type Config struct { 25 | SecretKey string `yaml:"secret_key"` 26 | Duration time.Duration `yaml:"duration"` 27 | } 28 | 29 | // Service represents the JWT service 30 | type Service struct { 31 | config Config 32 | } 33 | 34 | // NewService creates a new JWT service 35 | func NewService(config Config) *Service { 36 | return &Service{ 37 | config: config, 38 | } 39 | } 40 | 41 | // GenerateToken generates a new JWT token 42 | func (s *Service) GenerateToken(userID uint, username string, role string) (string, error) { 43 | claims := &Claims{ 44 | UserID: userID, 45 | Username: username, 46 | Role: role, 47 | RegisteredClaims: jwt.RegisteredClaims{ 48 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.config.Duration)), 49 | IssuedAt: jwt.NewNumericDate(time.Now()), 50 | NotBefore: jwt.NewNumericDate(time.Now()), 51 | }, 52 | } 53 | 54 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 55 | return token.SignedString([]byte(s.config.SecretKey)) 56 | } 57 | 58 | // ValidateToken validates a JWT token 59 | func (s *Service) ValidateToken(tokenString string) (*Claims, error) { 60 | token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { 61 | return []byte(s.config.SecretKey), nil 62 | }) 63 | 64 | if err != nil { 65 | if errors.Is(err, jwt.ErrTokenExpired) { 66 | return nil, ErrExpiredToken 67 | } 68 | return nil, ErrInvalidToken 69 | } 70 | 71 | if claims, ok := token.Claims.(*Claims); ok && token.Valid { 72 | return claims, nil 73 | } 74 | 75 | return nil, ErrInvalidToken 76 | } 77 | -------------------------------------------------------------------------------- /internal/auth/oauth2/client.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Claims represents the JWT claims 8 | type Claims map[string]any 9 | 10 | // Client defines the interface for OAuth2 client 11 | type Client interface { 12 | // ValidateToken validates the token and returns the claims 13 | ValidateToken(ctx context.Context, token string) (Claims, error) 14 | } 15 | 16 | // WithClaims stores claims in context 17 | func WithClaims(ctx context.Context, claims Claims) context.Context { 18 | return context.WithValue(ctx, claimsKey{}, claims) 19 | } 20 | 21 | // GetClaims retrieves claims from context 22 | func GetClaims(ctx context.Context) (Claims, bool) { 23 | claims, ok := ctx.Value(claimsKey{}).(Claims) 24 | return claims, ok 25 | } 26 | 27 | type claimsKey struct{} 28 | -------------------------------------------------------------------------------- /internal/auth/types/authenticator.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // Authenticator defines the interface for authentication 9 | type Authenticator interface { 10 | // Authenticate authenticates the request 11 | Authenticate(ctx context.Context, r *http.Request) error 12 | } 13 | 14 | // Mode represents the authentication mode 15 | type Mode string 16 | 17 | const ( 18 | // ModeNone represents no authentication 19 | ModeNone Mode = "none" 20 | // ModeBearer represents bearer token authentication 21 | ModeBearer Mode = "bearer" 22 | // ModeAPIKey represents API key authentication 23 | ModeAPIKey Mode = "apikey" 24 | ) 25 | -------------------------------------------------------------------------------- /internal/auth/types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // ContextKeyUsername is the context key for username 4 | const ContextKeyUsername = "username" 5 | 6 | // ContextKeyPassword is the context key for password 7 | const ContextKeyPassword = "password" 8 | -------------------------------------------------------------------------------- /internal/common/cnst/action.go: -------------------------------------------------------------------------------- 1 | package cnst 2 | 3 | // ActionType represents the type of action performed on a configuration 4 | type ActionType string 5 | 6 | const ( 7 | // ActionCreate represents a create action 8 | ActionCreate ActionType = "Create" 9 | // ActionUpdate represents an update action 10 | ActionUpdate ActionType = "Update" 11 | // ActionDelete represents a delete action 12 | ActionDelete ActionType = "Delete" 13 | // ActionRevert represents a revert action 14 | ActionRevert ActionType = "Revert" 15 | ) 16 | -------------------------------------------------------------------------------- /internal/common/cnst/app.go: -------------------------------------------------------------------------------- 1 | package cnst 2 | 3 | const ( 4 | AppName = "mcp-gateway" 5 | CommandName = "mcp-gateway" 6 | ) 7 | -------------------------------------------------------------------------------- /internal/common/cnst/config.go: -------------------------------------------------------------------------------- 1 | package cnst 2 | 3 | const ( 4 | ApiServerYaml = "apiserver.yaml" 5 | MCPGatewayYaml = "mcp-gateway.yaml" 6 | ) 7 | -------------------------------------------------------------------------------- /internal/common/cnst/errors.go: -------------------------------------------------------------------------------- 1 | package cnst 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrDuplicateToolName is returned when a tool name is duplicated 7 | ErrDuplicateToolName = errors.New("duplicate tool name") 8 | // ErrDuplicateServerName is returned when a server name is duplicated 9 | ErrDuplicateServerName = errors.New("duplicate server name") 10 | // ErrDuplicateRouterPrefix is returned when a router prefix is duplicated 11 | ErrDuplicateRouterPrefix = errors.New("duplicate router prefix") 12 | 13 | // ErrNotReceiver is returned when a notifier cannot receive updates 14 | ErrNotReceiver = errors.New("notifier cannot receive updates") 15 | // ErrNotSender is returned when a notifier cannot send updates 16 | ErrNotSender = errors.New("notifier cannot send updates") 17 | ) 18 | -------------------------------------------------------------------------------- /internal/common/cnst/i18n.go: -------------------------------------------------------------------------------- 1 | package cnst 2 | 3 | const ( 4 | LangDefault = LangEN 5 | LangEN = "en" 6 | LangZH = "zh" 7 | 8 | XLang = "X-Lang" 9 | CtxKeyTranslator = "translator" 10 | ) 11 | -------------------------------------------------------------------------------- /internal/common/cnst/policy.go: -------------------------------------------------------------------------------- 1 | package cnst 2 | 3 | // MCPStartupPolicy represents the startup policy for MCP servers 4 | type MCPStartupPolicy string 5 | 6 | const ( 7 | // PolicyOnStart represents the policy to connect on server start 8 | PolicyOnStart MCPStartupPolicy = "onStart" 9 | // PolicyOnDemand represents the policy to connect when needed 10 | PolicyOnDemand MCPStartupPolicy = "onDemand" 11 | ) 12 | -------------------------------------------------------------------------------- /internal/common/cnst/proto.go: -------------------------------------------------------------------------------- 1 | package cnst 2 | 3 | type ProtoType string 4 | 5 | const ( 6 | BackendProtoStdio ProtoType = "stdio" 7 | BackendProtoSSE ProtoType = "sse" 8 | BackendProtoStreamable ProtoType = "streamable-http" 9 | BackendProtoHttp ProtoType = "http" 10 | BackendProtoGrpc ProtoType = "grpc" 11 | ) 12 | 13 | const ( 14 | FrontendProtoSSE ProtoType = "sse" 15 | ) 16 | 17 | func (s ProtoType) String() string { 18 | return string(s) 19 | } 20 | -------------------------------------------------------------------------------- /internal/common/config/apiserver.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type ( 9 | APIServerConfig struct { 10 | Database DatabaseConfig `yaml:"database"` 11 | OpenAI OpenAIConfig `yaml:"openai"` 12 | Storage StorageConfig `yaml:"storage"` 13 | Notifier NotifierConfig `yaml:"notifier"` 14 | Logger LoggerConfig `yaml:"logger"` 15 | JWT JWTConfig `yaml:"jwt"` 16 | SuperAdmin SuperAdminConfig `yaml:"super_admin"` 17 | I18n I18nConfig `yaml:"i18n"` 18 | } 19 | 20 | // I18nConfig represents the internationalization configuration 21 | I18nConfig struct { 22 | Path string `yaml:"path"` // Path to i18n translation files 23 | } 24 | 25 | DatabaseConfig struct { 26 | Type string `yaml:"type"` // mysql, postgres, sqlite, etc. 27 | Host string `yaml:"host"` // localhost 28 | Port int `yaml:"port"` // 3306 (for mysql), 5432 (for postgres) 29 | User string `yaml:"user"` // root (for mysql), postgres (for postgres) 30 | Password string `yaml:"password"` // password 31 | DBName string `yaml:"dbname"` // database name 32 | SSLMode string `yaml:"sslmode"` // disable (for postgres) 33 | } 34 | 35 | OpenAIConfig struct { 36 | APIKey string `yaml:"api_key"` 37 | Model string `yaml:"model"` 38 | BaseURL string `yaml:"base_url"` 39 | } 40 | 41 | JWTConfig struct { 42 | SecretKey string `yaml:"secret_key"` 43 | Duration time.Duration `yaml:"duration"` 44 | } 45 | ) 46 | 47 | // GetDSN returns the database connection string 48 | func (c *DatabaseConfig) GetDSN() string { 49 | switch c.Type { 50 | case "postgres": 51 | return c.getPostgresDSN() 52 | case "mysql": 53 | return c.getMySQLDSN() 54 | case "sqlite": 55 | return c.DBName // For SQLite, DBName is the file path 56 | default: 57 | return "" 58 | } 59 | } 60 | 61 | // getPostgresDSN returns PostgreSQL connection string 62 | func (c *DatabaseConfig) getPostgresDSN() string { 63 | return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", 64 | c.User, c.Password, c.Host, c.Port, c.DBName, c.SSLMode) 65 | } 66 | 67 | // getMySQLDSN returns MySQL connection string 68 | func (c *DatabaseConfig) getMySQLDSN() string { 69 | return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", 70 | c.User, c.Password, c.Host, c.Port, c.DBName) 71 | } 72 | -------------------------------------------------------------------------------- /internal/common/config/notifier.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type ( 4 | // NotifierConfig represents the configuration for notifier 5 | NotifierConfig struct { 6 | Role string `yaml:"role"` // receiver, sender, or both 7 | Type string `yaml:"type"` 8 | Signal SignalConfig `yaml:"signal"` 9 | API APIConfig `yaml:"api"` 10 | Redis RedisConfig `yaml:"redis"` 11 | } 12 | 13 | // SignalConfig represents the configuration for signal-based notifier 14 | SignalConfig struct { 15 | Signal string `yaml:"signal"` 16 | PID string `yaml:"pid"` 17 | } 18 | 19 | // APIConfig represents the configuration for API-based notifier 20 | APIConfig struct { 21 | Port int `yaml:"port"` 22 | TargetURL string `yaml:"target_url"` 23 | } 24 | 25 | // RedisConfig represents the configuration for Redis-based notifier 26 | RedisConfig struct { 27 | Addr string `yaml:"addr"` 28 | Username string `yaml:"username"` 29 | Password string `yaml:"password"` 30 | DB int `yaml:"db"` 31 | Topic string `yaml:"topic"` 32 | } 33 | ) 34 | 35 | // NotifierRole represents the role of a notifier 36 | type NotifierRole string 37 | 38 | const ( 39 | // RoleReceiver represents a notifier that can only receive updates 40 | RoleReceiver NotifierRole = "receiver" 41 | // RoleSender represents a notifier that can only send updates 42 | RoleSender NotifierRole = "sender" 43 | // RoleBoth represents a notifier that can both send and receive updates 44 | RoleBoth NotifierRole = "both" 45 | ) 46 | -------------------------------------------------------------------------------- /internal/common/config/storage.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "time" 4 | 5 | type ( 6 | StorageConfig struct { 7 | Type string `yaml:"type"` // disk or db 8 | RevisionHistoryLimit int `yaml:"revision_history_limit"` // number of versions to keep 9 | Database DatabaseConfig `yaml:"database"` // database configuration for db type 10 | Disk DiskStorageConfig `yaml:"disk"` // disk configuration for disk type 11 | API APIStorageConfig `yaml:"api"` // disk configuration for api type 12 | } 13 | 14 | DiskStorageConfig struct { 15 | Path string `yaml:"path"` // path for disk storage 16 | } 17 | 18 | APIStorageConfig struct { 19 | Url string `yaml:"url"` // http url for api 20 | ConfigJSONPath string `yaml:"configJSONPath"` // configJSONPath for config in http response 21 | Timeout time.Duration `yaml:"timeout"` // timeout for http request 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /internal/common/dto/websocket.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | // WebSocketMessage represents a message sent over WebSocket 4 | type WebSocketMessage struct { 5 | Type string `json:"type"` 6 | Content string `json:"content"` 7 | Sender string `json:"sender"` 8 | Timestamp int64 `json:"timestamp"` 9 | ID string `json:"id"` 10 | Tools []Tool `json:"tools,omitempty"` 11 | ToolResult *ToolResult `json:"toolResult,omitempty"` 12 | } 13 | 14 | // WebSocketResponse represents a response sent over WebSocket 15 | type WebSocketResponse struct { 16 | Type string `json:"type"` 17 | Content string `json:"content"` 18 | Sender string `json:"sender"` 19 | Timestamp int64 `json:"timestamp"` 20 | ID string `json:"id"` 21 | ToolCalls []ToolCall `json:"toolCalls,omitempty"` 22 | } 23 | 24 | // ToolParameters represents the parameters of a tool 25 | type ToolParameters struct { 26 | Properties map[string]interface{} `json:"properties"` 27 | Required []string `json:"required"` 28 | } 29 | 30 | // ToolFunction represents the function details of a tool call 31 | type ToolFunction struct { 32 | Name string `json:"name"` 33 | Arguments string `json:"arguments"` 34 | } 35 | 36 | // Tool represents a tool that can be called by the LLM 37 | type Tool struct { 38 | Name string `json:"name"` 39 | Description string `json:"description"` 40 | Parameters ToolParameters `json:"parameters"` 41 | } 42 | 43 | // ToolCall represents a tool call from the LLM 44 | type ToolCall struct { 45 | ID string `json:"id"` 46 | Type string `json:"type"` 47 | Function ToolFunction `json:"function"` 48 | } 49 | 50 | // ToolCallResponse represents the response for a tool call 51 | type ToolCallResponse struct { 52 | Name string `json:"name"` 53 | Arguments map[string]interface{} `json:"arguments"` 54 | } 55 | 56 | // ToolResult represents the result of a tool call 57 | type ToolResult struct { 58 | Name string `json:"name"` 59 | Result string `json:"result"` 60 | ToolCallID string `json:"toolCallId"` 61 | } 62 | 63 | // MsgType represents the type of WebSocket message 64 | const ( 65 | MsgTypeMessage = "message" 66 | MsgTypeStream = "stream" 67 | MsgTypeToolCall = "tool_call" 68 | MsgTypeToolResult = "tool_result" 69 | ) 70 | -------------------------------------------------------------------------------- /internal/core/mcpproxy/common.go: -------------------------------------------------------------------------------- 1 | package mcpproxy 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | mcpgo "github.com/mark3labs/mcp-go/mcp" 7 | "github.com/mcp-ecosystem/mcp-gateway/pkg/mcp" 8 | ) 9 | 10 | // convertMCPGoResult converts mcp-go result to local mcp format 11 | func convertMCPGoResult(res *mcpgo.CallToolResult) *mcp.CallToolResult { 12 | result := &mcp.CallToolResult{ 13 | IsError: res.IsError, 14 | } 15 | 16 | // Process content items 17 | if len(res.Content) > 0 { 18 | var validContents []mcp.Content 19 | 20 | for _, content := range res.Content { 21 | // Skip null content 22 | if content == nil { 23 | continue 24 | } 25 | 26 | // Try to get content type 27 | contentType := "" 28 | switch c := content.(type) { 29 | case *mcpgo.TextContent: 30 | contentType = "text" 31 | validContents = append(validContents, &mcp.TextContent{ 32 | Type: "text", 33 | Text: c.Text, 34 | }) 35 | case *mcpgo.ImageContent: 36 | contentType = "image" 37 | validContents = append(validContents, &mcp.ImageContent{ 38 | Type: "image", 39 | Data: c.Data, 40 | MimeType: c.MIMEType, 41 | }) 42 | case *mcpgo.AudioContent: 43 | contentType = "audio" 44 | validContents = append(validContents, &mcp.AudioContent{ 45 | Type: "audio", 46 | Data: c.Data, 47 | MimeType: c.MIMEType, 48 | }) 49 | default: 50 | // Try to parse from raw content 51 | rawContent, err := json.Marshal(content) 52 | if err == nil { 53 | var contentMap map[string]interface{} 54 | if json.Unmarshal(rawContent, &contentMap) == nil { 55 | if typ, ok := contentMap["type"].(string); ok { 56 | contentType = typ 57 | 58 | switch contentType { 59 | case "text": 60 | if text, ok := contentMap["text"].(string); ok { 61 | validContents = append(validContents, &mcp.TextContent{ 62 | Type: "text", 63 | Text: text, 64 | }) 65 | } 66 | case "image": 67 | data, _ := contentMap["data"].(string) 68 | mimeType, _ := contentMap["mimeType"].(string) 69 | validContents = append(validContents, &mcp.ImageContent{ 70 | Type: "image", 71 | Data: data, 72 | MimeType: mimeType, 73 | }) 74 | case "audio": 75 | data, _ := contentMap["data"].(string) 76 | mimeType, _ := contentMap["mimeType"].(string) 77 | validContents = append(validContents, &mcp.AudioContent{ 78 | Type: "audio", 79 | Data: data, 80 | MimeType: mimeType, 81 | }) 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | if len(validContents) > 0 { 90 | result.Content = validContents 91 | } 92 | } 93 | 94 | return result 95 | } 96 | -------------------------------------------------------------------------------- /internal/core/mcpproxy/transport.go: -------------------------------------------------------------------------------- 1 | package mcpproxy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/mcp-ecosystem/mcp-gateway/internal/common/config" 8 | "github.com/mcp-ecosystem/mcp-gateway/internal/template" 9 | "github.com/mcp-ecosystem/mcp-gateway/pkg/mcp" 10 | ) 11 | 12 | // TransportType represents the type of transport 13 | type TransportType string 14 | 15 | const ( 16 | // TypeSSE represents SSE-based transport 17 | TypeSSE TransportType = "sse" 18 | // TypeStdio represents stdio-based transport 19 | TypeStdio TransportType = "stdio" 20 | // TypeStreamable represents streamable HTTP-based transport 21 | TypeStreamable TransportType = "streamable-http" 22 | ) 23 | 24 | // Transport defines the interface for MCP transport implementations 25 | type Transport interface { 26 | // FetchTools fetches the list of available tools 27 | FetchTools(ctx context.Context) ([]mcp.ToolSchema, error) 28 | 29 | // CallTool invokes a tool 30 | CallTool(ctx context.Context, params mcp.CallToolParams, req *template.RequestWrapper) (*mcp.CallToolResult, error) 31 | 32 | // Start starts the transport 33 | Start(ctx context.Context, tmplCtx *template.Context) error 34 | 35 | // Stop stops the transport 36 | Stop(ctx context.Context) error 37 | 38 | // IsRunning returns true if the transport is running 39 | IsRunning() bool 40 | } 41 | 42 | // NewTransport creates transport based on the configuration 43 | func NewTransport(cfg config.MCPServerConfig) (Transport, error) { 44 | switch TransportType(cfg.Type) { 45 | case TypeSSE: 46 | return &SSETransport{cfg: cfg}, nil 47 | case TypeStdio: 48 | return &StdioTransport{cfg: cfg}, nil 49 | case TypeStreamable: 50 | return &StreamableTransport{cfg: cfg}, nil 51 | default: 52 | return nil, fmt.Errorf("unknown transport type: %s", cfg.Type) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/core/storage.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Storage defines the interface for storing runtimeUnit data 9 | type Storage interface { 10 | // SaveTool saves a tool configuration 11 | SaveTool(ctx context.Context, tool *Tool) error 12 | 13 | // GetTool retrieves a tool configuration 14 | GetTool(ctx context.Context, name string) (*Tool, error) 15 | 16 | // ListTools lists all tool configurations 17 | ListTools(ctx context.Context) ([]*Tool, error) 18 | 19 | // DeleteTool deletes a tool configuration 20 | DeleteTool(ctx context.Context, name string) error 21 | 22 | // SaveServer saves a server configuration 23 | SaveServer(ctx context.Context, server *StoredServer) error 24 | 25 | // GetServer retrieves a server configuration 26 | GetServer(ctx context.Context, name string) (*StoredServer, error) 27 | 28 | // ListServers lists all server configurations 29 | ListServers(ctx context.Context) ([]*StoredServer, error) 30 | 31 | // DeleteServer deletes a server configuration 32 | DeleteServer(ctx context.Context, name string) error 33 | } 34 | 35 | // Tool represents a tool in storage 36 | type Tool struct { 37 | Name string `json:"name"` 38 | Description string `json:"description"` 39 | Method string `json:"method"` 40 | Endpoint string `json:"endpoint"` 41 | Headers map[string]string `json:"headers"` 42 | Args []Arg `json:"args"` 43 | RequestBody string `json:"requestBody"` 44 | ResponseBody string `json:"responseBody"` 45 | CreatedAt time.Time `json:"createdAt"` 46 | UpdatedAt time.Time `json:"updatedAt"` 47 | } 48 | 49 | // StoredServer represents a server in storage 50 | type StoredServer struct { 51 | Name string `json:"name"` 52 | Description string `json:"description"` 53 | Auth Auth `json:"auth"` 54 | AllowedTools []string `json:"allowedTools"` 55 | AllowedOrigins []string `json:"allowedOrigins"` 56 | CreatedAt time.Time `json:"createdAt"` 57 | UpdatedAt time.Time `json:"updatedAt"` 58 | } 59 | 60 | // Arg represents a tool argument 61 | type Arg struct { 62 | Name string `json:"name"` 63 | Position string `json:"position"` 64 | Required bool `json:"required"` 65 | Type string `json:"type"` 66 | Description string `json:"description"` 67 | Default string `json:"default"` 68 | } 69 | 70 | // Auth represents authentication configuration 71 | type Auth struct { 72 | Mode string `json:"mode"` 73 | Header string `json:"header"` 74 | ArgKey string `json:"argKey"` 75 | } 76 | -------------------------------------------------------------------------------- /internal/mcp/session/factory.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.uber.org/zap" 7 | 8 | "github.com/mcp-ecosystem/mcp-gateway/internal/common/config" 9 | ) 10 | 11 | // Type represents the type of session store 12 | type Type string 13 | 14 | const ( 15 | // TypeMemory represents in-memory session store 16 | TypeMemory Type = "memory" 17 | // TypeRedis represents Redis-based session store 18 | TypeRedis Type = "redis" 19 | ) 20 | 21 | // NewStore creates a new session store based on configuration 22 | func NewStore(logger *zap.Logger, cfg *config.SessionConfig) (Store, error) { 23 | logger.Info("Initializing session store", zap.String("type", cfg.Type)) 24 | switch Type(cfg.Type) { 25 | case TypeMemory: 26 | return NewMemoryStore(logger), nil 27 | case TypeRedis: 28 | return NewRedisStore(logger, cfg.Redis) 29 | default: 30 | return nil, fmt.Errorf("unsupported session store type: %s", cfg.Type) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/mcp/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Message represents a unified message structure for session communication. 9 | type Message struct { 10 | Event string // Event type, e.g., "message", "close", "ping" 11 | Data []byte // Payload 12 | } 13 | 14 | // RequestInfo holds information about the request that created the session. 15 | type RequestInfo struct { 16 | Headers map[string]string `json:"headers"` 17 | Query map[string]string `json:"query"` 18 | Cookies map[string]string `json:"cookies"` 19 | } 20 | 21 | // Meta holds immutable metadata about a session. 22 | type Meta struct { 23 | ID string `json:"id"` // Unique session ID 24 | CreatedAt time.Time `json:"created_at"` // Timestamp of session creation 25 | Prefix string `json:"prefix"` // Optional namespace or application prefix 26 | Type string `json:"type"` // Connection type, e.g., "sse", "streamable" 27 | Request *RequestInfo `json:"request"` // Request information 28 | Extra []byte `json:"extra"` // Optional serialized extra data 29 | } 30 | 31 | // Connection represents an active session connection capable of sending messages. 32 | type Connection interface { 33 | // EventQueue returns a read-only channel where outbound messages are published. 34 | EventQueue() <-chan *Message 35 | 36 | // Send pushes a message to the session. 37 | Send(ctx context.Context, msg *Message) error 38 | 39 | // Close gracefully terminates the session connection. 40 | Close(ctx context.Context) error 41 | 42 | // Meta returns metadata associated with the session. 43 | Meta() *Meta 44 | } 45 | 46 | // Store manages the lifecycle and lookup of active session connections. 47 | type Store interface { 48 | // Register creates and registers a new session connection. 49 | Register(ctx context.Context, meta *Meta) (Connection, error) 50 | 51 | // Get retrieves an active session connection by ID. 52 | Get(ctx context.Context, id string) (Connection, error) 53 | 54 | // Unregister removes a session connection by ID. 55 | Unregister(ctx context.Context, id string) error 56 | 57 | // List returns all currently active session connections. 58 | List(ctx context.Context) ([]Connection, error) 59 | } 60 | -------------------------------------------------------------------------------- /internal/mcp/storage/factory.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.uber.org/zap" 7 | 8 | "github.com/mcp-ecosystem/mcp-gateway/internal/common/config" 9 | ) 10 | 11 | // NewStore creates a new store based on configuration 12 | func NewStore(logger *zap.Logger, cfg *config.StorageConfig) (Store, error) { 13 | logger.Info("Initializing storage", zap.String("type", cfg.Type)) 14 | switch cfg.Type { 15 | case "disk": 16 | return NewDiskStore(logger, cfg) 17 | case "db": 18 | return NewDBStore(logger, cfg) 19 | case "api": 20 | return NewAPIStore(logger, cfg.API.Url, cfg.API.ConfigJSONPath, cfg.API.Timeout) 21 | default: 22 | return nil, fmt.Errorf("unsupported storage type: %s", cfg.Type) 23 | } 24 | } 25 | 26 | // buildDSN builds the database connection string based on configuration 27 | func buildDSN(cfg *config.DatabaseConfig) (string, error) { 28 | switch cfg.Type { 29 | case "postgres": 30 | return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", 31 | cfg.Host, 32 | cfg.Port, 33 | cfg.User, 34 | cfg.Password, 35 | cfg.DBName, 36 | cfg.SSLMode), nil 37 | case "mysql": 38 | return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", 39 | cfg.User, 40 | cfg.Password, 41 | cfg.Host, 42 | cfg.Port, 43 | cfg.DBName), nil 44 | case "sqlite": 45 | return cfg.DBName, nil 46 | default: 47 | return "", fmt.Errorf("unsupported database type: %s", cfg.Type) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/mcp/storage/notifier/factory.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "go.uber.org/zap" 8 | 9 | "github.com/mcp-ecosystem/mcp-gateway/internal/common/config" 10 | ) 11 | 12 | // Type represents the type of notifier 13 | type Type string 14 | 15 | const ( 16 | // TypeSignal represents signal-based notifier 17 | TypeSignal Type = "signal" 18 | // TypeAPI represents API-based notifier 19 | TypeAPI Type = "api" 20 | // TypeRedis represents Redis-based notifier 21 | TypeRedis Type = "redis" 22 | // TypeComposite represents composite notifier 23 | TypeComposite Type = "composite" 24 | ) 25 | 26 | // NewNotifier creates a new notifier based on the configuration 27 | func NewNotifier(ctx context.Context, logger *zap.Logger, cfg *config.NotifierConfig) (Notifier, error) { 28 | role := config.NotifierRole(cfg.Role) 29 | if role == "" { 30 | role = config.RoleBoth // Default to both if not specified 31 | } 32 | 33 | switch Type(cfg.Type) { 34 | case TypeSignal: 35 | return NewSignalNotifier(ctx, logger, cfg.Signal.PID, role), nil 36 | case TypeAPI: 37 | return NewAPINotifier(logger, cfg.API.Port, role, cfg.API.TargetURL), nil 38 | case TypeRedis: 39 | return NewRedisNotifier(logger, cfg.Redis.Addr, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.DB, cfg.Redis.Topic, role) 40 | case TypeComposite: 41 | notifiers := make([]Notifier, 0) 42 | // Add signal notifier 43 | signalNotifier := NewSignalNotifier(ctx, logger, cfg.Signal.PID, role) 44 | notifiers = append(notifiers, signalNotifier) 45 | // Add API notifier 46 | apiNotifier := NewAPINotifier(logger, cfg.API.Port, role, cfg.API.TargetURL) 47 | notifiers = append(notifiers, apiNotifier) 48 | // Add Redis notifier if configured 49 | if cfg.Redis.Addr != "" { 50 | redisNotifier, err := NewRedisNotifier(logger, cfg.Redis.Addr, cfg.Redis.Username, cfg.Redis.Password, cfg.Redis.DB, cfg.Redis.Topic, role) 51 | if err != nil { 52 | return nil, err 53 | } 54 | notifiers = append(notifiers, redisNotifier) 55 | } 56 | return NewCompositeNotifier(ctx, logger, notifiers...), nil 57 | default: 58 | return nil, fmt.Errorf("unknown notifier type: %s", cfg.Type) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/mcp/storage/notifier/notifier.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/mcp-ecosystem/mcp-gateway/internal/common/config" 7 | ) 8 | 9 | // Notifier defines the interface for configuration update notification 10 | type Notifier interface { 11 | // Watch returns a channel that receives notifications when servers are updated 12 | Watch(ctx context.Context) (<-chan *config.MCPConfig, error) 13 | 14 | // NotifyUpdate triggers an update notification 15 | NotifyUpdate(ctx context.Context, updated *config.MCPConfig) error 16 | 17 | // CanReceive returns true if the notifier can receive updates 18 | CanReceive() bool 19 | 20 | // CanSend returns true if the notifier can send updates 21 | CanSend() bool 22 | } 23 | -------------------------------------------------------------------------------- /internal/mcp/storage/notifier/redis.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/mcp-ecosystem/mcp-gateway/internal/common/cnst" 9 | 10 | "github.com/go-redis/redis/v8" 11 | "go.uber.org/zap" 12 | 13 | "github.com/mcp-ecosystem/mcp-gateway/internal/common/config" 14 | ) 15 | 16 | // RedisNotifier implements Notifier using Redis pub/sub 17 | type RedisNotifier struct { 18 | logger *zap.Logger 19 | client *redis.Client 20 | topic string 21 | role config.NotifierRole 22 | } 23 | 24 | // NewRedisNotifier creates a new Redis-based notifier 25 | func NewRedisNotifier(logger *zap.Logger, addr, username, password string, db int, topic string, role config.NotifierRole) (*RedisNotifier, error) { 26 | client := redis.NewClient(&redis.Options{ 27 | Addr: addr, 28 | Username: username, 29 | Password: password, 30 | DB: db, 31 | }) 32 | 33 | // Test connection 34 | if err := client.Ping(context.Background()).Err(); err != nil { 35 | return nil, fmt.Errorf("failed to connect to Redis: %w", err) 36 | } 37 | 38 | return &RedisNotifier{ 39 | logger: logger.Named("notifier.redis"), 40 | client: client, 41 | topic: topic, 42 | role: role, 43 | }, nil 44 | } 45 | 46 | // Watch implements Notifier.Watch 47 | func (r *RedisNotifier) Watch(ctx context.Context) (<-chan *config.MCPConfig, error) { 48 | if !r.CanReceive() { 49 | return nil, cnst.ErrNotReceiver 50 | } 51 | 52 | ch := make(chan *config.MCPConfig, 10) 53 | 54 | pubsub := r.client.Subscribe(ctx, r.topic) 55 | go func() { 56 | defer close(ch) 57 | defer pubsub.Close() 58 | 59 | for msg := range pubsub.Channel() { 60 | var cfg config.MCPConfig 61 | if err := json.Unmarshal([]byte(msg.Payload), &cfg); err == nil { 62 | select { 63 | case ch <- &cfg: 64 | case <-ctx.Done(): 65 | return 66 | } 67 | } 68 | } 69 | }() 70 | 71 | return ch, nil 72 | } 73 | 74 | // NotifyUpdate implements Notifier.NotifyUpdate 75 | func (r *RedisNotifier) NotifyUpdate(ctx context.Context, server *config.MCPConfig) error { 76 | if !r.CanSend() { 77 | return cnst.ErrNotSender 78 | } 79 | 80 | data, err := json.Marshal(server) 81 | if err != nil { 82 | return fmt.Errorf("failed to marshal server config: %w", err) 83 | } 84 | 85 | return r.client.Publish(ctx, r.topic, data).Err() 86 | } 87 | 88 | // CanReceive returns true if the notifier can receive updates 89 | func (r *RedisNotifier) CanReceive() bool { 90 | return r.role == config.RoleReceiver || r.role == config.RoleBoth 91 | } 92 | 93 | // CanSend returns true if the notifier can send updates 94 | func (r *RedisNotifier) CanSend() bool { 95 | return r.role == config.RoleSender || r.role == config.RoleBoth 96 | } 97 | -------------------------------------------------------------------------------- /internal/mcp/storage/store.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/mcp-ecosystem/mcp-gateway/internal/common/config" 8 | ) 9 | 10 | // Store defines the interface for MCP configuration storage 11 | type Store interface { 12 | // Create creates a new MCP configuration 13 | Create(ctx context.Context, cfg *config.MCPConfig) error 14 | 15 | // Get gets an MCP configuration by tenant and name 16 | Get(ctx context.Context, tenant, name string, includeDeleted ...bool) (*config.MCPConfig, error) 17 | 18 | // List lists all MCP configurations 19 | // includeDeleted: if true, includes soft deleted records 20 | List(ctx context.Context, includeDeleted ...bool) ([]*config.MCPConfig, error) 21 | 22 | // ListUpdated lists all MCP configurations updated since a given time 23 | ListUpdated(ctx context.Context, since time.Time) ([]*config.MCPConfig, error) 24 | 25 | // Update updates an existing MCP configuration 26 | Update(ctx context.Context, cfg *config.MCPConfig) error 27 | 28 | // Delete deletes an MCP configuration by tenant and name 29 | Delete(ctx context.Context, tenant, name string) error 30 | 31 | // GetVersion gets a specific version of the configuration by tenant and name 32 | GetVersion(ctx context.Context, tenant, name string, version int) (*config.MCPConfigVersion, error) 33 | 34 | // ListVersions lists all versions of a configuration by tenant and name 35 | ListVersions(ctx context.Context, tenant, name string) ([]*config.MCPConfigVersion, error) 36 | 37 | // DeleteVersion deletes a specific version by tenant and name 38 | DeleteVersion(ctx context.Context, tenant, name string, version int) error 39 | 40 | // SetActiveVersion sets a specific version as the active version by tenant and name 41 | SetActiveVersion(ctx context.Context, tenant, name string, version int) error 42 | } 43 | -------------------------------------------------------------------------------- /internal/template/context.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import "os" 4 | 5 | // Context represents the template context 6 | type ( 7 | Context struct { 8 | Args map[string]any `json:"args"` 9 | Config map[string]string `json:"config"` 10 | Request RequestWrapper `json:"request"` 11 | Response ResponseWrapper `json:"response"` 12 | Env func(string) string `json:"-"` // Function to get environment variables 13 | } 14 | RequestWrapper struct { 15 | Headers map[string]string `json:"headers"` 16 | Query map[string]string `json:"query"` 17 | Cookies map[string]string `json:"cookies"` 18 | Path map[string]string `json:"path"` 19 | Body map[string]any `json:"body"` 20 | } 21 | ResponseWrapper struct { 22 | Data any `json:"data"` 23 | Body any `json:"body"` 24 | } 25 | ) 26 | 27 | // NewContext creates a new template context 28 | func NewContext() *Context { 29 | return &Context{ 30 | Args: make(map[string]any), 31 | Config: make(map[string]string), 32 | Request: RequestWrapper{ 33 | Headers: make(map[string]string), 34 | Query: make(map[string]string), 35 | Cookies: make(map[string]string), 36 | Path: make(map[string]string), 37 | Body: make(map[string]any), 38 | }, 39 | Response: ResponseWrapper{}, 40 | Env: os.Getenv, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/template/funcs.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import "encoding/json" 4 | 5 | func fromJSON(s string) []map[string]any { 6 | var result []map[string]any 7 | _ = json.Unmarshal([]byte(s), &result) 8 | return result 9 | } 10 | 11 | func toJSON(v any) (string, error) { 12 | b, err := json.Marshal(v) 13 | if err != nil { 14 | return "", err 15 | } 16 | return string(b), nil 17 | } 18 | -------------------------------------------------------------------------------- /pkg/helper/config.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | // GetCfgPath returns the path to the configuration file. 9 | // 10 | // Priority: 11 | // 1. If filename is an absolute path, return it directly. 12 | // 2. Check ./{filename} and ./configs/{filename} 13 | // 3. Otherwise, fallback to /etc/mcp-gateway/{filename} 14 | func GetCfgPath(filename string) string { 15 | if filename == "" { 16 | panic("filename cannot be empty") 17 | } 18 | 19 | if filepath.IsAbs(filename) { 20 | return filename 21 | } 22 | 23 | currentDir := getCurrentDir(filename) 24 | if currentDir != "" { 25 | return currentDir 26 | } 27 | 28 | // fallback 29 | return filepath.Join("/etc/mcp-gateway", filename) 30 | } 31 | 32 | func getCurrentDir(filename string) string { 33 | currentDir, err := os.Getwd() 34 | if err != nil || currentDir == "" { 35 | return "" 36 | } 37 | 38 | candidatePath := filepath.Join(currentDir, filename) 39 | _, err = os.Stat(candidatePath) 40 | if err == nil { 41 | absPath, err := filepath.Abs(candidatePath) 42 | if err == nil { 43 | return absPath 44 | } 45 | } 46 | 47 | candidatePath = filepath.Join(currentDir, "configs", filename) 48 | _, err = os.Stat(candidatePath) 49 | if err == nil { 50 | absPath, err := filepath.Abs(candidatePath) 51 | if err == nil { 52 | return absPath 53 | } 54 | } 55 | return "" 56 | } 57 | -------------------------------------------------------------------------------- /pkg/helper/pid.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | // GetPIDPath returns the path to the PID file. 9 | // 10 | // Priority: 11 | // 1. If filename is an absolute path, return it directly. 12 | // 2. Check ./{filename} and ./configs/{filename} 13 | // 3. Otherwise, fallback to /var/run/mcp-gateway/{filename} 14 | func GetPIDPath(filename string) string { 15 | if filepath.IsAbs(filename) { 16 | return filename 17 | } 18 | 19 | currentDir := getPIDCurrentDir(filename) 20 | if currentDir != "" { 21 | return currentDir 22 | } 23 | 24 | // fallback 25 | return filepath.Join("/var/run/mcp-gateway.pid") 26 | } 27 | 28 | func getPIDCurrentDir(filename string) string { 29 | if filename == "" { 30 | return "" 31 | } 32 | 33 | currentDir, err := os.Getwd() 34 | if err != nil || currentDir == "" { 35 | return "" 36 | } 37 | 38 | candidatePath := filepath.Join(currentDir, filename) 39 | absPath, err := filepath.Abs(candidatePath) 40 | if err != nil { 41 | return "" 42 | } 43 | 44 | // Check if parent directory exists 45 | parentDir := filepath.Dir(absPath) 46 | if _, err := os.Stat(parentDir); err == nil { 47 | return absPath 48 | } 49 | 50 | return "" 51 | } 52 | -------------------------------------------------------------------------------- /pkg/mcp/cnst.go: -------------------------------------------------------------------------------- 1 | package mcp 2 | 3 | // Protocol versions 4 | const ( 5 | ProtocolVersion20250326 = "2025-03-26" 6 | ProtocolVersion20241105 = "2024-11-05" 7 | LatestProtocolVersion = ProtocolVersion20241105 8 | JSPNRPCVersion = "2.0" 9 | ) 10 | 11 | // Methods 12 | const ( 13 | Initialize = "initialize" 14 | NotificationInitialized = "notifications/initialized" 15 | Ping = "ping" 16 | ToolsList = "tools/list" 17 | ToolsCall = "tools/call" 18 | ) 19 | 20 | // Response 21 | const ( 22 | Accepted = "Accepted" 23 | 24 | NotificationRootsListChanged = "notifications/roots/list_changed" 25 | NotificationCancelled = "notifications/cancelled" 26 | NotificationProgress = "notifications/progress" 27 | NotificationMessage = "notifications/message" 28 | NotificationResourceUpdated = "notifications/resources/updated" 29 | NotificationResourceListChanged = "notifications/resources/list_changed" 30 | NotificationToolListChanged = "notifications/tools/list_changed" 31 | NotificationPromptListChanged = "notifications/prompts/list_changed" 32 | 33 | SamplingCreateMessage = "sampling/createMessage" 34 | LoggingSetLevel = "logging/setLevel" 35 | 36 | PromptsGet = "prompts/get" 37 | PromptsList = "prompts/list" 38 | ResourcesList = "resources/list" 39 | ResourcesTemplatesList = "resources/templates/list" 40 | ResourcesRead = "resources/read" 41 | ) 42 | 43 | // Error codes for MCP protocol 44 | // Standard JSON-RPC error codes 45 | const ( 46 | ErrorCodeParseError = -32700 47 | ErrorCodeInvalidRequest = -32600 48 | ErrorCodeMethodNotFound = -32601 49 | ErrorCodeInvalidParams = -32602 50 | ErrorCodeInternalError = -32603 51 | ) 52 | 53 | // SDKs and applications error codes 54 | const ( 55 | ErrorCodeConnectionClosed = -32000 56 | ErrorCodeRequestTimeout = -32001 57 | ) 58 | 59 | const ( 60 | HeaderMcpSessionID = "Mcp-Session-Id" 61 | ) 62 | 63 | const ( 64 | TextContentType = "text" 65 | ImageContentType = "image" 66 | AudioContentType = "audio" 67 | ) 68 | -------------------------------------------------------------------------------- /pkg/openai/client.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/mcp-ecosystem/mcp-gateway/internal/common/config" 7 | 8 | "github.com/openai/openai-go" 9 | "github.com/openai/openai-go/option" 10 | "github.com/openai/openai-go/packages/ssestream" 11 | ) 12 | 13 | // Client wraps the OpenAI client with our configuration 14 | type Client struct { 15 | client openai.Client 16 | model string 17 | } 18 | 19 | // NewClient creates a new OpenAI client with the given API key 20 | func NewClient(cfg *config.OpenAIConfig) *Client { 21 | client := openai.NewClient( 22 | option.WithAPIKey(cfg.APIKey), 23 | option.WithBaseURL(cfg.BaseURL), 24 | ) 25 | 26 | return &Client{ 27 | client: client, 28 | model: cfg.Model, 29 | } 30 | } 31 | 32 | // ChatCompletion handles chat completion requests 33 | func (c *Client) ChatCompletion(ctx context.Context, messages []openai.ChatCompletionMessageParamUnion) (*openai.ChatCompletion, error) { 34 | // Create chat completion request 35 | chatCompletion, err := c.client.Chat.Completions.New( 36 | ctx, 37 | openai.ChatCompletionNewParams{ 38 | Messages: messages, 39 | Model: c.model, 40 | }, 41 | ) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return chatCompletion, nil 47 | } 48 | 49 | // ChatCompletionStream handles streaming chat completion requests 50 | func (c *Client) ChatCompletionStream(ctx context.Context, messages []openai.ChatCompletionMessageParamUnion, tools []openai.ChatCompletionToolParam) (*ssestream.Stream[openai.ChatCompletionChunk], error) { 51 | // Create streaming chat completion request 52 | params := openai.ChatCompletionNewParams{ 53 | Messages: messages, 54 | Model: c.model, 55 | } 56 | 57 | // Add tools if provided 58 | if len(tools) > 0 { 59 | params.Tools = tools 60 | } 61 | 62 | stream := c.client.Chat.Completions.NewStreaming( 63 | ctx, 64 | params, 65 | ) 66 | 67 | return stream, stream.Err() 68 | } 69 | -------------------------------------------------------------------------------- /pkg/utils/env.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "fmt" 4 | 5 | // MapToEnvList converts a map to a slice of "key=value" strings. 6 | func MapToEnvList(env map[string]string) []string { 7 | envList := make([]string, 0, len(env)) 8 | for k, v := range env { 9 | envList = append(envList, fmt.Sprintf("%s=%s", k, v)) 10 | } 11 | return envList 12 | } 13 | -------------------------------------------------------------------------------- /pkg/utils/env_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMapToEnvList(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | input map[string]string 13 | expected []string 14 | }{ 15 | { 16 | name: "empty map", 17 | input: map[string]string{}, 18 | expected: []string{}, 19 | }, 20 | { 21 | name: "single key-value pair", 22 | input: map[string]string{ 23 | "KEY": "value", 24 | }, 25 | expected: []string{"KEY=value"}, 26 | }, 27 | { 28 | name: "multiple key-value pairs", 29 | input: map[string]string{ 30 | "KEY1": "value1", 31 | "KEY2": "value2", 32 | "KEY3": "value3", 33 | }, 34 | expected: []string{"KEY1=value1", "KEY2=value2", "KEY3=value3"}, 35 | }, 36 | } 37 | 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | result := MapToEnvList(tt.input) 41 | assert.ElementsMatch(t, tt.expected, result) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/utils/pid.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // PIDManager handles PID file operations 10 | type PIDManager struct { 11 | pidFile string 12 | } 13 | 14 | // NewPIDManager creates a new PIDManager instance 15 | func NewPIDManager(pidFile string) *PIDManager { 16 | return &PIDManager{ 17 | pidFile: pidFile, 18 | } 19 | } 20 | 21 | // NewPIDManagerFromConfig creates a new PIDManager instance from config 22 | func NewPIDManagerFromConfig(pidFile string) *PIDManager { 23 | return NewPIDManager(pidFile) 24 | } 25 | 26 | // WritePID writes the current process ID to the PID file 27 | func (p *PIDManager) WritePID() error { 28 | dir := filepath.Dir(p.pidFile) 29 | if err := os.MkdirAll(dir, 0755); err != nil { 30 | return fmt.Errorf("failed to create PID directory: %w", err) 31 | } 32 | 33 | pid := os.Getpid() 34 | return os.WriteFile(p.pidFile, []byte(fmt.Sprintf("%d\n", pid)), 0644) 35 | } 36 | 37 | // RemovePID removes the PID file 38 | func (p *PIDManager) RemovePID() error { 39 | return os.Remove(p.pidFile) 40 | } 41 | 42 | // GetPIDFile returns the PID file path 43 | func (p *PIDManager) GetPIDFile() string { 44 | return p.pidFile 45 | } 46 | -------------------------------------------------------------------------------- /pkg/utils/pid_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestPIDManager(t *testing.T) { 15 | tmpDir := t.TempDir() 16 | pidFile := filepath.Join(tmpDir, "test.pid") 17 | 18 | t.Run("NewPIDManager", func(t *testing.T) { 19 | manager := NewPIDManager(pidFile) 20 | assert.Equal(t, pidFile, manager.GetPIDFile()) 21 | }) 22 | 23 | t.Run("NewPIDManagerFromConfig", func(t *testing.T) { 24 | manager := NewPIDManagerFromConfig(pidFile) 25 | assert.Equal(t, pidFile, manager.GetPIDFile()) 26 | }) 27 | 28 | t.Run("WritePID", func(t *testing.T) { 29 | manager := NewPIDManager(pidFile) 30 | err := manager.WritePID() 31 | require.NoError(t, err) 32 | 33 | content, err := os.ReadFile(pidFile) 34 | require.NoError(t, err) 35 | 36 | pidStr := strings.TrimSpace(string(content)) 37 | pid, err := strconv.Atoi(pidStr) 38 | require.NoError(t, err) 39 | assert.Equal(t, os.Getpid(), pid) 40 | }) 41 | 42 | t.Run("RemovePID", func(t *testing.T) { 43 | manager := NewPIDManager(pidFile) 44 | err := manager.WritePID() 45 | require.NoError(t, err) 46 | 47 | err = manager.RemovePID() 48 | require.NoError(t, err) 49 | 50 | _, err = os.Stat(pidFile) 51 | assert.True(t, os.IsNotExist(err)) 52 | }) 53 | 54 | t.Run("WritePID with non-existent directory", func(t *testing.T) { 55 | manager := NewPIDManager(filepath.Join(tmpDir, "subdir", "test.pid")) 56 | err := manager.WritePID() 57 | require.NoError(t, err) 58 | 59 | _, err = os.Stat(manager.GetPIDFile()) 60 | require.NoError(t, err) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /pkg/utils/process.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "syscall" 9 | ) 10 | 11 | // SendSignalToPIDFile sends a signal to the process identified by the PID file 12 | func SendSignalToPIDFile(pidFile string, sig syscall.Signal) error { 13 | if pidFile == "" { 14 | return fmt.Errorf("PID file path is empty") 15 | } 16 | 17 | pid, err := readPIDFile(pidFile) 18 | if err != nil { 19 | return fmt.Errorf("failed to read PID file: %w", err) 20 | } 21 | 22 | if err := signalProcess(pid, sig); err != nil { 23 | return fmt.Errorf("failed to signal process: %w", err) 24 | } 25 | 26 | return nil 27 | } 28 | 29 | // readPIDFile reads and parses the PID file 30 | func readPIDFile(pidFile string) (int, error) { 31 | pidBytes, err := os.ReadFile(pidFile) 32 | if err != nil { 33 | return 0, fmt.Errorf("failed to read PID file: %w", err) 34 | } 35 | 36 | pid, err := strconv.Atoi(strings.TrimSpace(string(pidBytes))) 37 | if err != nil { 38 | return 0, fmt.Errorf("invalid PID format in file: %w", err) 39 | } 40 | 41 | if pid <= 0 { 42 | return 0, fmt.Errorf("invalid PID value: %d", pid) 43 | } 44 | 45 | return pid, nil 46 | } 47 | 48 | // signalProcess finds the process and sends the specified signal 49 | func signalProcess(pid int, sig syscall.Signal) error { 50 | process, err := os.FindProcess(pid) 51 | if err != nil { 52 | return fmt.Errorf("process not found: %w", err) 53 | } 54 | 55 | if err := process.Signal(sig); err != nil { 56 | return fmt.Errorf("failed to send signal: %w", err) 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /pkg/utils/process_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "golang.org/x/sys/unix" 12 | ) 13 | 14 | func TestProcessFunctions(t *testing.T) { 15 | tmpDir := t.TempDir() 16 | pidFile := filepath.Join(tmpDir, "test.pid") 17 | 18 | t.Run("SendSignalToPIDFile with empty path", func(t *testing.T) { 19 | err := SendSignalToPIDFile("", unix.SIGTERM) 20 | assert.Error(t, err) 21 | assert.Contains(t, err.Error(), "PID file path is empty") 22 | }) 23 | 24 | t.Run("SendSignalToPIDFile with non-existent file", func(t *testing.T) { 25 | err := SendSignalToPIDFile("non_existent.pid", unix.SIGTERM) 26 | assert.Error(t, err) 27 | assert.Contains(t, err.Error(), "failed to read PID file") 28 | }) 29 | 30 | t.Run("readPIDFile with invalid content", func(t *testing.T) { 31 | err := os.WriteFile(pidFile, []byte("invalid"), 0644) 32 | require.NoError(t, err) 33 | 34 | _, err = readPIDFile(pidFile) 35 | assert.Error(t, err) 36 | assert.Contains(t, err.Error(), "invalid PID format") 37 | }) 38 | 39 | t.Run("readPIDFile with invalid PID value", func(t *testing.T) { 40 | err := os.WriteFile(pidFile, []byte("0"), 0644) 41 | require.NoError(t, err) 42 | 43 | _, err = readPIDFile(pidFile) 44 | assert.Error(t, err) 45 | assert.Contains(t, err.Error(), "invalid PID value") 46 | }) 47 | 48 | t.Run("readPIDFile with valid PID", func(t *testing.T) { 49 | pid := os.Getpid() 50 | err := os.WriteFile(pidFile, []byte(strconv.Itoa(pid)), 0644) 51 | require.NoError(t, err) 52 | 53 | readPID, err := readPIDFile(pidFile) 54 | assert.NoError(t, err) 55 | assert.Equal(t, pid, readPID) 56 | }) 57 | 58 | t.Run("signalProcess with non-existent PID", func(t *testing.T) { 59 | err := signalProcess(999999, unix.SIGTERM) 60 | assert.Error(t, err) 61 | assert.Contains(t, err.Error(), "failed to send signal") 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/utils/str.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func FirstNonEmpty(str1, str2 string) string { 4 | if str1 != "" { 5 | return str1 6 | } 7 | if str2 != "" { 8 | return str2 9 | } 10 | return "" 11 | } 12 | -------------------------------------------------------------------------------- /pkg/version/VERSION: -------------------------------------------------------------------------------- 1 | v0.5.1 -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | _ "embed" 5 | ) 6 | 7 | //go:embed VERSION 8 | var version string 9 | 10 | // Get returns the current version of the application 11 | func Get() string { 12 | return version 13 | } 14 | -------------------------------------------------------------------------------- /web/.env.example: -------------------------------------------------------------------------------- 1 | # API and WebSocket URLs 2 | VITE_API_BASE_URL=/api 3 | VITE_WS_BASE_URL=/api/ws 4 | VITE_MCP_GATEWAY_BASE_URL=/mcp 5 | VITE_BASE_URL=/ 6 | VITE_DEV_API_BASE_URL=http://localhost:5234 -------------------------------------------------------------------------------- /web/.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*@heroui/* -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # React + Tailwind 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. One top of the standard Vite setup, [tailwindcss](https://tailwindcss.com/) is installed and ready to be used in React components. 4 | 5 | Additional references: 6 | 7 | - [Getting started with Vite](https://vitejs.dev/guide/) 8 | - [Tailwind documentation](https://tailwindcss.com/docs/installation) 9 | -------------------------------------------------------------------------------- /web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import tseslint from '@typescript-eslint/eslint-plugin'; 3 | import tseslintParser from '@typescript-eslint/parser'; 4 | import reactPlugin from 'eslint-plugin-react'; 5 | import reactHooksPlugin from 'eslint-plugin-react-hooks'; 6 | import importPlugin from 'eslint-plugin-import'; 7 | import jsxA11yPlugin from 'eslint-plugin-jsx-a11y'; 8 | 9 | export default [ 10 | { ignores: ['dist/**', 'node_modules/**', 'build/**'] }, 11 | js.configs.recommended, 12 | { 13 | files: ['**/*.{js,jsx,ts,tsx}'], 14 | ignores: [ 15 | 'node_modules/**', 16 | 'dist/**', 17 | 'build/**', 18 | '*.config.js', 19 | '*.config.ts', 20 | 'eslint.config.js', 21 | '.eslintrc.js', 22 | '.eslintrc.json' 23 | ], 24 | languageOptions: { 25 | parser: tseslintParser, 26 | parserOptions: { 27 | ecmaFeatures: { 28 | jsx: true 29 | }, 30 | ecmaVersion: 'latest', 31 | sourceType: 'module' 32 | }, 33 | globals: { 34 | document: 'readonly', 35 | window: 'readonly', 36 | console: 'readonly', 37 | fetch: 'readonly', 38 | WebSocket: 'readonly', 39 | HTMLElement: 'readonly', 40 | HTMLDivElement: 'readonly', 41 | MouseEvent: 'readonly', 42 | navigator: 'readonly', 43 | __dirname: 'readonly', 44 | URL: 'readonly', 45 | File: 'readonly', 46 | FileReader: 'readonly', 47 | Blob: 'readonly', 48 | HTMLInputElement: 'readonly', 49 | MutationObserver: 'readonly', 50 | MutationRecord: 'readonly', 51 | } 52 | }, 53 | plugins: { 54 | '@typescript-eslint': tseslint, 55 | 'react': reactPlugin, 56 | 'react-hooks': reactHooksPlugin, 57 | 'import': importPlugin, 58 | 'jsx-a11y': jsxA11yPlugin 59 | }, 60 | rules: { 61 | ...tseslint.configs.recommended.rules, 62 | ...reactPlugin.configs.recommended.rules, 63 | ...reactHooksPlugin.configs.recommended.rules, 64 | ...jsxA11yPlugin.configs.recommended.rules, 65 | 'react/react-in-jsx-scope': 'off', 66 | 'react/prop-types': 'off', 67 | '@typescript-eslint/explicit-module-boundary-types': 'off', 68 | '@typescript-eslint/no-explicit-any': 'warn', 69 | '@typescript-eslint/no-unused-vars': ['error', { 70 | 'argsIgnorePattern': '^_', 71 | 'varsIgnorePattern': '^_' 72 | }], 73 | 'import/order': [ 74 | 'error', 75 | { 76 | groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 77 | 'newlines-between': 'always', 78 | alphabetize: { 79 | order: 'asc', 80 | caseInsensitive: true 81 | } 82 | } 83 | ] 84 | }, 85 | settings: { 86 | react: { 87 | version: 'detect' 88 | } 89 | } 90 | } 91 | ]; 92 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MCP Gateway 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-admin-dashboard", 3 | "private": true, 4 | "version": "0.5.1", 5 | "type": "module", 6 | "description": "MCP Gateway Admin Dashboard - A modern web interface for managing MCP Gateway services", 7 | "keywords": [ 8 | "mcp", 9 | "gateway", 10 | "admin", 11 | "dashboard", 12 | "react", 13 | "typescript" 14 | ], 15 | "homepage": "https://github.com/mcp-ecosystem/mcp-gateway#readme", 16 | "bugs": { 17 | "url": "https://github.com/mcp-ecosystem/mcp-gateway/issues" 18 | }, 19 | "main": "dist/index.js", 20 | "types": "dist/index.d.ts", 21 | "files": [ 22 | "dist", 23 | "README.md" 24 | ], 25 | "author": { 26 | "name": "Leo", 27 | "url": "https://github.com/ifuryst" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/mcp-ecosystem/mcp-gateway.git" 32 | }, 33 | "publishConfig": { 34 | "access": "public" 35 | }, 36 | "license": "MIT", 37 | "scripts": { 38 | "dev": "vite", 39 | "build": "tsc && vite build", 40 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 41 | "preview": "vite preview" 42 | }, 43 | "dependencies": { 44 | "@heroui/react": "2.7.8", 45 | "@iconify/react": "latest", 46 | "@modelcontextprotocol/sdk": "^1.10.1", 47 | "@monaco-editor/react": "^4.7.0", 48 | "@types/uuid": "^10.0.0", 49 | "axios": "^1.8.4", 50 | "copy-to-clipboard": "^3.3.3", 51 | "dayjs": "^1.11.13", 52 | "framer-motion": "^11.18.2", 53 | "i18next": "^25.1.1", 54 | "i18next-browser-languagedetector": "^8.1.0", 55 | "js-yaml": "^4.1.0", 56 | "monaco-editor": "^0.52.2", 57 | "monaco-yaml": "^5.3.1", 58 | "react": "^18.3.1", 59 | "react-diff-viewer-continued": "^3.4.0", 60 | "react-dom": "^18.3.1", 61 | "react-dropzone": "^14.3.8", 62 | "react-i18next": "^15.5.1", 63 | "react-markdown": "^10.1.0", 64 | "react-router-dom": "^6.30.0", 65 | "rehype-highlight": "^7.0.2", 66 | "rehype-katex": "^7.0.1", 67 | "remark-gfm": "^4.0.1", 68 | "remark-math": "^6.0.0", 69 | "uuid": "^11.1.0", 70 | "zod": "^3.24.4" 71 | }, 72 | "devDependencies": { 73 | "@eslint/js": "^9.24.0", 74 | "@types/js-yaml": "^4.0.9", 75 | "@types/node": "^22.15.3", 76 | "@types/react": "^18.3.18", 77 | "@types/react-dom": "^18.3.5", 78 | "@typescript-eslint/eslint-plugin": "^8.30.1", 79 | "@typescript-eslint/parser": "^8.30.1", 80 | "@vitejs/plugin-react": "^4.3.4", 81 | "autoprefixer": "10.4.20", 82 | "eslint": "^9.24.0", 83 | "eslint-plugin-import": "^2.31.0", 84 | "eslint-plugin-jsx-a11y": "^6.10.2", 85 | "eslint-plugin-react": "^7.37.5", 86 | "eslint-plugin-react-hooks": "^5.2.0", 87 | "postcss": "8.4.49", 88 | "tailwindcss": "3.4.17", 89 | "typescript": "5.7.3", 90 | "vite": "^6.3.4" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /web/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp-ecosystem/mcp-gateway/a722a86a70c80ec5d048aea2e645247895c28b66/web/public/logo.png -------------------------------------------------------------------------------- /web/public/wechat-qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcp-ecosystem/mcp-gateway/a722a86a70c80ec5d048aea2e645247895c28b66/web/public/wechat-qrcode.png -------------------------------------------------------------------------------- /web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom'; 3 | 4 | import { Layout } from './components/Layout'; 5 | import { LoginPage } from './pages/auth/login'; 6 | import { ChatInterface } from './pages/chat/chat-interface'; 7 | import { ConfigVersionsPage } from './pages/gateway/config-versions'; 8 | import { GatewayManager } from './pages/gateway/gateway-manager'; 9 | import { TenantManagement } from './pages/users/tenant-management'; 10 | import { UserManagement } from './pages/users/user-management'; 11 | 12 | // Route guard component 13 | function PrivateRoute({ children }: { children: React.ReactNode }) { 14 | const location = useLocation(); 15 | const token = window.localStorage.getItem('token'); 16 | 17 | if (!token) { 18 | return ; 19 | } 20 | 21 | return <>{children}; 22 | } 23 | 24 | // Main layout component 25 | function MainLayout() { 26 | return ( 27 | 28 | 29 | } /> 30 | } /> 31 | } /> 32 | } /> 33 | } /> 34 | } /> 35 | } /> 36 | } /> 37 | } /> 38 | 39 | 40 | ); 41 | } 42 | 43 | export default function App() { 44 | return ( 45 | 49 | 50 | } /> 51 | 55 | 56 | 57 | } 58 | /> 59 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /web/src/components/LanguageSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@heroui/react'; 2 | import { Icon } from '@iconify/react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | const languages = [ 6 | { code: 'en', name: 'English' }, 7 | { code: 'zh', name: '中文' } 8 | ]; 9 | 10 | /** 11 | * Language switching component that allows users to change the application language. 12 | * When language is changed, it automatically updates i18n context and all 13 | * subsequent API requests will include the selected language in X-Lang header. 14 | */ 15 | export function LanguageSwitcher() { 16 | const { i18n, t } = useTranslation(); 17 | 18 | /** 19 | * Change the application language 20 | * This automatically affects API requests through the axios interceptor 21 | * which adds the X-Lang header to all requests 22 | */ 23 | const handleLanguageChange = (languageCode: string) => { 24 | i18n.changeLanguage(languageCode); 25 | }; 26 | 27 | const currentLanguage = languages.find(lang => lang.code === i18n.language) || languages[0]; 28 | 29 | return ( 30 | 31 | 32 | 39 | 40 | 41 | {languages.map((lang) => ( 42 | handleLanguageChange(lang.code)} 45 | className={i18n.language === lang.code ? 'bg-primary-100' : ''} 46 | > 47 | {lang.name} 48 | 49 | ))} 50 | 51 | 52 | ); 53 | } -------------------------------------------------------------------------------- /web/src/components/WechatQRCode.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ModalContent, 3 | ModalHeader, 4 | ModalBody, 5 | ModalFooter, 6 | Button, 7 | } from "@heroui/react"; 8 | import { useTranslation } from 'react-i18next'; 9 | 10 | import { AccessibleModal } from "./AccessibleModal"; 11 | 12 | // 导入二维码图片 13 | import wechatQrcode from '/wechat-qrcode.png'; 14 | 15 | interface WechatQRCodeProps { 16 | isOpen: boolean; 17 | onOpenChange: (isOpen: boolean) => void; 18 | } 19 | 20 | export function WechatQRCode({ isOpen, onOpenChange }: WechatQRCodeProps) { 21 | const { t } = useTranslation(); 22 | 23 | return ( 24 | 25 | 26 | {t('common.join_wechat')} 27 | 28 |
29 | WeChat QR Code 34 |

35 | {t('common.scan_qrcode')} 36 |

37 |

38 | {t('common.add_wechat_note')} 39 |

40 |
41 |
42 | 43 | 46 | 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /web/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_MCP_GATEWAY_BASE_URL: string 5 | readonly VITE_API_BASE_URL: string 6 | readonly VITE_WS_BASE_URL: string 7 | } 8 | 9 | interface ImportMeta { 10 | readonly env: ImportMetaEnv 11 | } 12 | -------------------------------------------------------------------------------- /web/src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | export function useTheme() { 4 | const [isDark, setIsDark] = useState(() => { 5 | const savedTheme = window.localStorage.getItem('theme'); 6 | return savedTheme === 'dark'; 7 | }); 8 | 9 | useEffect(() => { 10 | const observer = new MutationObserver((mutations) => { 11 | mutations.forEach((mutation) => { 12 | if (mutation.attributeName === 'class') { 13 | setIsDark(document.documentElement.classList.contains('dark')); 14 | } 15 | }); 16 | }); 17 | 18 | observer.observe(document.documentElement, { 19 | attributes: true, 20 | attributeFilter: ['class'] 21 | }); 22 | 23 | return () => observer.disconnect(); 24 | }, []); 25 | 26 | return { isDark }; 27 | } -------------------------------------------------------------------------------- /web/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import LanguageDetector from 'i18next-browser-languagedetector'; 3 | import { initReactI18next } from 'react-i18next'; 4 | 5 | // Import translations 6 | import translationEN from './locales/en/translation.json'; 7 | import translationZH from './locales/zh/translation.json'; 8 | 9 | declare const process: { 10 | env: { 11 | NODE_ENV: string; 12 | }; 13 | }; 14 | 15 | const resources = { 16 | en: { 17 | translation: translationEN, 18 | }, 19 | zh: { 20 | translation: translationZH, 21 | }, 22 | }; 23 | 24 | i18n 25 | .use(LanguageDetector) 26 | .use(initReactI18next) 27 | .init({ 28 | resources, 29 | fallbackLng: 'zh', 30 | debug: process.env.NODE_ENV === 'development', 31 | interpolation: { 32 | escapeValue: false, 33 | }, 34 | }); 35 | 36 | export default i18n; -------------------------------------------------------------------------------- /web/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 210 40% 98%; 8 | --foreground: 222.2 84% 4.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 222.2 84% 4.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 222.2 84% 4.9%; 13 | --primary: 221.2 83.2% 53.3%; 14 | --primary-foreground: 210 40% 98%; 15 | --secondary: 210 40% 96.1%; 16 | --secondary-foreground: 222.2 47.4% 11.2%; 17 | --muted: 210 40% 96.1%; 18 | --muted-foreground: 215.4 16.3% 46.9%; 19 | --accent: 210 40% 96.1%; 20 | --accent-foreground: 222.2 47.4% 11.2%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 210 40% 98%; 23 | --border: 214.3 31.8% 91.4%; 24 | --input: 214.3 31.8% 91.4%; 25 | --ring: 221.2 83.2% 53.3%; 26 | --radius: 0.5rem; 27 | --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); 28 | } 29 | 30 | .dark { 31 | --background: 220 13% 18%; /* 23272E - Main window background */ 32 | --foreground: 220 13% 95%; 33 | --card: 220 13% 15%; /* 1E2228 - Cards and sidebar */ 34 | --card-foreground: 220 13% 95%; 35 | --popover: 220 13% 15%; /* 1E2228 */ 36 | --popover-foreground: 220 13% 95%; 37 | --primary: 217.2 91.2% 59.8%; 38 | --primary-foreground: 210 40% 98%; 39 | --secondary: 220 13% 13%; /* 1D1F23 - Secondary elements */ 40 | --secondary-foreground: 220 13% 95%; 41 | --muted: 220 13% 13%; /* 1D1F23 */ 42 | --muted-foreground: 220 13% 70%; 43 | --accent: 220 13% 13%; /* 1D1F23 */ 44 | --accent-foreground: 220 13% 95%; 45 | --destructive: 0 84.2% 60.2%; 46 | --destructive-foreground: 210 40% 98%; 47 | --border: 220 13% 25%; 48 | --input: 220 13% 25%; 49 | --ring: 217.2 91.2% 59.8%; 50 | --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3); 51 | } 52 | } 53 | 54 | @layer utilities { 55 | .scrollbar-hide::-webkit-scrollbar { 56 | display: none; 57 | } 58 | .scrollbar-hide { 59 | -ms-overflow-style: none; 60 | scrollbar-width: none; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import {HeroUIProvider, ToastProvider} from "@heroui/react"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom/client"; 4 | 5 | import App from "./App.tsx"; 6 | import './i18n'; 7 | 8 | import "./index.css"; 9 | 10 | ReactDOM.createRoot(document.getElementById("root")!).render( 11 | 12 | 13 | 14 |
15 | 16 |
17 |
18 |
, 19 | ); 20 | -------------------------------------------------------------------------------- /web/src/pages/chat/chat-context.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Message } from '../../types/message'; 4 | 5 | interface ChatContextType { 6 | messages: Message[]; 7 | } 8 | 9 | export const ChatContext = React.createContext({ 10 | messages: [], 11 | }); 12 | 13 | export function ChatProvider({ children, messages }: { children: React.ReactNode; messages: Message[] }) { 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | } -------------------------------------------------------------------------------- /web/src/pages/gateway/components/OpenAPIImport.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardBody, Button } from '@heroui/react'; 2 | import { Icon } from '@iconify/react'; 3 | import { t } from 'i18next'; 4 | import React, { useCallback } from 'react'; 5 | import { useDropzone } from 'react-dropzone'; 6 | 7 | import { importOpenAPI } from '../../../services/api'; 8 | import { toast } from "../../../utils/toast.ts"; 9 | 10 | interface OpenAPIImportProps { 11 | onSuccess?: () => void; 12 | } 13 | 14 | const OpenAPIImport: React.FC = ({ onSuccess }) => { 15 | const onDrop = useCallback(async (acceptedFiles: globalThis.File[]) => { 16 | if (acceptedFiles.length === 0) { 17 | toast.error(t('errors.invalid_openapi_file'), { 18 | duration: 3000, 19 | }); 20 | return; 21 | } 22 | 23 | try { 24 | await importOpenAPI(acceptedFiles[0]); 25 | toast.success(t('errors.import_openapi_success'), { 26 | duration: 3000, 27 | }); 28 | onSuccess?.(); 29 | } catch { 30 | toast.error(t('errors.import_openapi_failed'), { 31 | duration: 3000, 32 | }) 33 | } 34 | }, [onSuccess]); 35 | 36 | const { getRootProps, getInputProps, isDragActive } = useDropzone({ 37 | onDrop, 38 | accept: { 39 | 'application/json': ['.json'], 40 | 'application/yaml': ['.yaml', '.yml'], 41 | 'text/yaml': ['.yaml', '.yml'] 42 | }, 43 | multiple: false 44 | }); 45 | 46 | return ( 47 | 48 | 49 |
55 | 56 | 57 | {isDragActive ? ( 58 |

Drop the OpenAPI specification file here...

59 | ) : ( 60 |
61 |

Drag and drop an OpenAPI specification file here

62 |

or

63 | 66 |
67 | )} 68 |

69 | Supported formats: JSON (.json), YAML (.yaml, .yml) 70 |

71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default OpenAPIImport; 78 | -------------------------------------------------------------------------------- /web/src/pages/gateway/constants/defaultConfig.ts: -------------------------------------------------------------------------------- 1 | import { Gateway } from '../../../types/gateway'; 2 | 3 | // Default configuration object for new or empty configurations 4 | export const defaultConfig: Gateway = { 5 | name: '', 6 | tenant: 'default', 7 | routers: [], 8 | servers: [], 9 | tools: [], 10 | mcpServers: [], 11 | createdAt: new Date().toISOString(), 12 | updatedAt: new Date().toISOString() 13 | }; 14 | -------------------------------------------------------------------------------- /web/src/types/gateway.ts: -------------------------------------------------------------------------------- 1 | export interface Tenant { 2 | id: number; 3 | name: string; 4 | prefix: string; 5 | description: string; 6 | isActive: boolean; 7 | } 8 | 9 | export interface ConfigEditorProps { 10 | config: string; 11 | onChange: (newConfig: string) => void; 12 | isDark: boolean; 13 | editorOptions: Record; 14 | isEditing?: boolean; 15 | } 16 | 17 | export interface Gateway { 18 | name: string; 19 | tenant: string; 20 | mcpServers?: MCPServerConfig[]; 21 | tools?: ToolConfig[]; 22 | servers?: ServerConfig[]; 23 | routers?: RouterConfig[]; 24 | createdAt: string; 25 | updatedAt: string; 26 | } 27 | 28 | export interface MCPServerConfig { 29 | type: string; 30 | name: string; 31 | command?: string; 32 | args?: string[]; 33 | env?: Record; 34 | url?: string; 35 | policy: string; 36 | preinstalled: boolean; 37 | } 38 | 39 | export interface ToolConfig { 40 | name: string; 41 | description?: string; 42 | method: string; 43 | endpoint: string; 44 | proxy?: ProxyConfig; 45 | headers?: Record; 46 | headersOrder?: string[]; 47 | args?: ArgConfig[]; 48 | requestBody: string; 49 | responseBody: string; 50 | inputSchema?: Record; 51 | } 52 | 53 | export interface ServerConfig { 54 | name: string; 55 | description: string; 56 | allowedTools?: string[]; 57 | config?: Record; 58 | } 59 | 60 | export interface RouterConfig { 61 | server: string; 62 | prefix: string; 63 | cors?: CORSConfig; 64 | } 65 | 66 | export interface CORSConfig { 67 | allowOrigins?: string[]; 68 | allowMethods?: string[]; 69 | allowHeaders?: string[]; 70 | exposeHeaders?: string[]; 71 | allowCredentials: boolean; 72 | } 73 | 74 | export interface ProxyConfig { 75 | host: string; 76 | port: number; 77 | type: string; 78 | } 79 | 80 | export interface ArgConfig { 81 | name: string; 82 | position: string; 83 | required: boolean; 84 | type: string; 85 | description: string; 86 | default: string; 87 | items?: ItemsConfig; 88 | } 89 | 90 | export interface ItemsConfig { 91 | type: string; 92 | enum?: string[]; 93 | } 94 | 95 | export interface KeyValueItem { 96 | key: string; 97 | value: string; 98 | description?: string; 99 | } 100 | 101 | export interface HeadersFormState { 102 | [toolIndex: number]: KeyValueItem[]; 103 | } 104 | 105 | export interface EnvFormState { 106 | [serverIndex: number]: KeyValueItem[]; 107 | } 108 | 109 | export interface YAMLConfig { 110 | name?: string; 111 | mcpServers?: Record; 112 | tools?: Record; 113 | servers?: Record; 114 | routers?: Record; 115 | [key: string]: unknown; 116 | } 117 | -------------------------------------------------------------------------------- /web/src/types/mcp.ts: -------------------------------------------------------------------------------- 1 | import {z} from 'zod'; 2 | 3 | export const ToolSchema = z 4 | .object({ 5 | name: z.string(), 6 | description: z.optional(z.string()), 7 | inputSchema: z 8 | .object({ 9 | type: z.literal("object"), 10 | properties: z.optional(z.object({}).passthrough()), 11 | }) 12 | .passthrough(), 13 | }) 14 | .passthrough(); 15 | 16 | export type Tool = z.infer; 17 | 18 | export const ListToolsResultSchema = z.object({ 19 | tools: z.array(ToolSchema), 20 | }); 21 | 22 | export type ListToolsResult = z.infer; 23 | 24 | export interface MCPConfigVersion { 25 | version: number; 26 | created_by: string; 27 | created_at: string; 28 | action_type: 'Create' | 'Update' | 'Delete' | 'Revert'; 29 | name: string; 30 | tenant: string; 31 | routers: string; 32 | servers: string; 33 | tools: string; 34 | mcp_servers: string; 35 | is_active: boolean; 36 | hash: string; 37 | } 38 | 39 | export interface MCPConfigVersionListResponse { 40 | data: MCPConfigVersion[]; 41 | } -------------------------------------------------------------------------------- /web/src/types/message.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | id: string; 3 | session_id: string; 4 | content: string; 5 | sender: 'user' | 'bot'; 6 | timestamp: string; 7 | isStreaming?: boolean; 8 | toolCalls?: ToolCall[]; 9 | toolResult?: ToolResult; 10 | } 11 | 12 | export interface BackendMessage { 13 | id: string; 14 | content: string; 15 | sender: string; 16 | timestamp: string; 17 | toolCalls?: string; 18 | toolResult?: string; 19 | } 20 | 21 | export interface ToolCall { 22 | id: string; 23 | type: string; 24 | function: { 25 | name: string; 26 | arguments: string; 27 | }; 28 | } 29 | 30 | export interface ToolResult { 31 | toolCallId: string; 32 | name: string; 33 | result: string; 34 | } 35 | -------------------------------------------------------------------------------- /web/src/types/monaco.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'monaco-editor' { 2 | export interface IEditorOptions { 3 | minimap?: { enabled: boolean }; 4 | fontSize?: number; 5 | lineNumbers?: 'on' | 'off'; 6 | roundedSelection?: boolean; 7 | scrollBeyondLastLine?: boolean; 8 | readOnly?: boolean; 9 | automaticLayout?: boolean; 10 | 'editor.background'?: string; 11 | 'editor.foreground'?: string; 12 | 'editor.lineHighlightBackground'?: string; 13 | 'editor.selectionBackground'?: string; 14 | 'editor.inactiveSelectionBackground'?: string; 15 | 'editor.lineHighlightBorder'?: string; 16 | 'editorCursor.foreground'?: string; 17 | 'editorWhitespace.foreground'?: string; 18 | } 19 | } 20 | 21 | declare global { 22 | interface Window { 23 | monaco: typeof import('monaco-editor'); 24 | } 25 | } -------------------------------------------------------------------------------- /web/src/types/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | username: string; 4 | role: 'admin' | 'normal'; 5 | isActive: boolean; 6 | createdAt: string; 7 | updatedAt: string; 8 | tenants?: Tenant[]; 9 | } 10 | 11 | export interface Tenant { 12 | id: number; 13 | name: string; 14 | prefix: string; 15 | description: string; 16 | isActive: boolean; 17 | createdAt: string; 18 | updatedAt: string; 19 | } 20 | 21 | export interface CreateUserForm { 22 | username: string; 23 | password: string; 24 | role: 'admin' | 'normal'; 25 | tenantIds?: number[]; 26 | } 27 | 28 | export interface UpdateUserForm { 29 | username: string; 30 | password?: string; 31 | role?: 'admin' | 'normal'; 32 | isActive?: boolean; 33 | tenantIds?: number[]; 34 | } 35 | 36 | export interface CreateTenantForm { 37 | name: string; 38 | prefix: string; 39 | description: string; 40 | } 41 | 42 | export interface UpdateTenantForm { 43 | name: string; 44 | prefix?: string; 45 | description?: string; 46 | isActive?: boolean; 47 | } -------------------------------------------------------------------------------- /web/src/utils/error-handler.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError } from 'axios'; 2 | import { t } from 'i18next'; 3 | 4 | import { toast } from './toast'; 5 | 6 | /** 7 | * Handle API errors and display user-friendly error messages 8 | * 9 | * @param error - The error object 10 | * @param fallbackMessage - Default message to display if specific error information is not available 11 | * @returns The error message that was displayed 12 | */ 13 | export const handleApiError = (error: unknown, fallbackMessage: string): string => { 14 | // Handle standard error responses 15 | if (axios.isAxiosError(error)) { 16 | const axiosError = error as AxiosError; 17 | const serverError = axiosError.response?.data as { error?: string }; 18 | 19 | if (serverError?.error) { 20 | // Server error message is already i18n translated 21 | toast.error(serverError.error, { duration: 3000 }); 22 | return serverError.error; 23 | } 24 | } 25 | 26 | // Handle general or network errors 27 | toast.error(t(fallbackMessage), { duration: 3000 }); 28 | return t(fallbackMessage); 29 | }; -------------------------------------------------------------------------------- /web/src/utils/i18n-utils.ts: -------------------------------------------------------------------------------- 1 | import i18n from '../i18n'; 2 | 3 | export const t = (key: string, options?: Record) => { 4 | return i18n.t(key, options); 5 | }; -------------------------------------------------------------------------------- /web/src/utils/toast.ts: -------------------------------------------------------------------------------- 1 | import { addToast } from "@heroui/react"; 2 | 3 | export const toast = { 4 | success: (message: string, options?: { description?: string; duration?: number }) => { 5 | addToast({ 6 | title: message, 7 | description: options?.description, 8 | // color: "success", 9 | timeout: options?.duration, 10 | }); 11 | }, 12 | error: (message: string, options?: { description?: string; duration?: number }) => { 13 | addToast({ 14 | title: message, 15 | description: options?.description, 16 | color: "danger", 17 | timeout: options?.duration, 18 | }); 19 | }, 20 | warning: (message: string, options?: { description?: string; duration?: number }) => { 21 | addToast({ 22 | title: message, 23 | description: options?.description, 24 | color: "warning", 25 | timeout: options?.duration, 26 | }); 27 | }, 28 | info: (message: string, options?: { description?: string; duration?: number }) => { 29 | addToast({ 30 | title: message, 31 | description: options?.description, 32 | color: "primary", 33 | timeout: options?.duration, 34 | }); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /web/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a random string of lowercase letters with specified length 3 | * @param length - The length of the random string to generate 4 | * @returns A random string containing only lowercase letters 5 | */ 6 | export function getRandomLetters(length: number): string { 7 | const letters = 'abcdefghijklmnopqrstuvwxyz'; 8 | let result = ''; 9 | for (let i = 0; i < length; i++) { 10 | result += letters.charAt(Math.floor(Math.random() * letters.length)); 11 | } 12 | return result; 13 | } -------------------------------------------------------------------------------- /web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | import { heroui } from "@heroui/react"; 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | export default { 6 | content: [ 7 | "./index.html", 8 | "./src/**/*.{js,ts,jsx,tsx}", 9 | "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}" 10 | ], 11 | darkMode: "class", 12 | theme: { 13 | extend: { 14 | colors: { 15 | border: "hsl(var(--border))", 16 | input: "hsl(var(--input))", 17 | ring: "hsl(var(--ring))", 18 | background: "hsl(var(--background))", 19 | foreground: "hsl(var(--foreground))", 20 | primary: { 21 | DEFAULT: "hsl(var(--primary))", 22 | foreground: "hsl(var(--primary-foreground))" 23 | }, 24 | secondary: { 25 | DEFAULT: "hsl(var(--secondary))", 26 | foreground: "hsl(var(--secondary-foreground))" 27 | }, 28 | destructive: { 29 | DEFAULT: "hsl(var(--destructive))", 30 | foreground: "hsl(var(--destructive-foreground))" 31 | }, 32 | muted: { 33 | DEFAULT: "hsl(var(--muted))", 34 | foreground: "hsl(var(--muted-foreground))" 35 | }, 36 | accent: { 37 | DEFAULT: "hsl(var(--accent))", 38 | foreground: "hsl(var(--accent-foreground))" 39 | }, 40 | popover: { 41 | DEFAULT: "hsl(var(--popover))", 42 | foreground: "hsl(var(--popover-foreground))" 43 | }, 44 | card: { 45 | DEFAULT: "hsl(var(--card))", 46 | foreground: "hsl(var(--card-foreground))" 47 | } 48 | }, 49 | borderRadius: { 50 | lg: "var(--radius)", 51 | md: "calc(var(--radius) - 2px)", 52 | sm: "calc(var(--radius) - 4px)" 53 | } 54 | } 55 | }, 56 | plugins: [heroui()] 57 | }; 58 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable", "WebWorker.ImportScripts", "ScriptHost", "WebWorker", "ESNext"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "typeRoots": ["./node_modules/@types", "./src/types"] 23 | }, 24 | "include": ["src"], 25 | "references": [{"path": "./tsconfig.node.json"}] 26 | } 27 | -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig, loadEnv } from "vite"; 3 | import pkg from './package.json'; 4 | import { fileURLToPath } from 'url'; 5 | import { dirname } from 'path'; 6 | 7 | const __dirname = dirname(fileURLToPath(import.meta.url)); 8 | 9 | export default defineConfig(({ mode }) => { 10 | // Load env file based on `mode` in the current working directory. 11 | // Set the third parameter to '' to load all env regardless of the `VITE_` prefix. 12 | const env = loadEnv(mode, __dirname, ''); 13 | 14 | return { 15 | base: env.VITE_BASE_URL || '/', 16 | plugins: [react()], 17 | define: { 18 | __APP_VERSION__: JSON.stringify(pkg.version), 19 | }, 20 | resolve: { 21 | alias: { 22 | '@': '/src', 23 | }, 24 | }, 25 | server: { 26 | allowedHosts: true, 27 | proxy: { 28 | '/api': { 29 | target: env.VITE_DEV_API_BASE_URL || '/api', 30 | changeOrigin: true, 31 | } 32 | }, 33 | }, 34 | }; 35 | }); 36 | --------------------------------------------------------------------------------