找回密码
 立即注册
首页 业界区 业界 如何用c# 做 mcp/ChatGPT app

如何用c# 做 mcp/ChatGPT app

蔡如风 昨天 15:20
什么是 mcp/ChatGPT app

我们需要理解 mcp 和 mcp/ChatGPT app 是什么?

  • MCP = 协议 / 标准
    是一种旨在解决该问题的开放标准。MCP 由 Anthropic 于 2024 年 11 月推出,为 LLM 与外部数据、应用和服务之间的通信提供一种安全且标准化的“语言”。它充当桥梁,使 AI 不再局限于静态知识,而成为一个能够检索当前信息并执行操作的动态智能体,从而提升其准确性、实用性与自动化能力。
    mcp 文档
  • mcp/ChatGPT App = 基于mcp这个标准接入 Chat 的应用形态 (ChatGPT App 应该只能在ChatGPT ,当然好像目前也是唯一一个提供app市场的)
    不过这里有两种

    • mcp apps
      更统一的方式,更多支持,比如 vscode 等等, 还有 MCP Inspector 调试工具支持,对本地开发会友好一些
    • chatgpt app sdk
      chatgpt 最早提出app sdk ,所以独有一套, 不过他们也支持了mcp apps mcp-apps-in-chatgpt

(当然现在 skills 是更多人使用龙虾这些agent 的方式,但是它不能提供 app 形式,所以这里无需讨论了)
所以我们可以直接使用 mcp apps 来做
MCP 有状态 vs 无状态

MCP 不是“必须有状态”,但它最初更偏向有状态会话模型

原因不是它做不了无状态,而是因为 MCP 想解决的不是普通的“一次请求,一次响应”问题,而是:

  • 持续上下文交换
  • 多轮工具调用
  • 流式输出
  • 服务端通知
  • 长任务协作
  • 初始化能力协商
所以,MCP 在设计上天然更适合“会话式交互”,这也是它看起来更像有状态协议的原因。
但在工程实现上,MCP 完全可以按场景做成:

  • 有状态(Stateful)
  • 无状态(Stateless)
整体结构对比

1)有状态(Stateful MCP)
  1. ┌────────────────────┐│     ChatGPT        ││   (MCP Client)     │└────────┬───────────┘         │ 1. init + OAuth         ▼┌──────────────────────────────┐│      MCP Server (Stateful)   ││ ┌──────────────────────────┐ ││ │ Session Store            │ ││ │ - session_id             │ ││ │ - user context           │ ││ │ - tool state             │ ││ │ - progress               │ ││ │ - auth/session cache     │ ││ └──────────────────────────┘ ││                              ││  Tools / Resources / Logic   │└────────┬─────────────────────┘         │         ▼   Backend APIs / DB
复制代码
核心: Server 会保存会话和上下文。
也就是说,服务端知道:

  • 当前是谁在调用
  • 前面执行到了哪一步
  • 当前工具链处于什么状态
  • 是否已经完成初始化协商
  • 是否已有可复用的授权信息
2)无状态(Stateless MCP)
  1. ┌────────────────────┐│     ChatGPT        ││   (MCP Client)     │└────────┬───────────┘         │ 每次请求都完整、自包含         ▼┌──────────────────────────────┐│     MCP Server (Stateless)   ││                              ││  No session storage          ││  No remembered context       ││                              ││  Tools / Resources / Logic   │└────────┬─────────────────────┘         │         ▼   Backend APIs / DB
复制代码
核心: Server 不保留状态。
每次请求都必须把本次执行所需的信息一起带上,例如:

  • Authorization
  • 用户标识
  • 当前参数
  • 回调信息
  • 本轮调用上下文
有状态流程(Stateful MCP)

流程图
  1. (1) ChatGPT → MCP    initialize(2) MCP → ChatGPT    返回 session_id = abc123--------------------------------(3) ChatGPT → MCP    tool: getOrders    headers:      Mcp-Session-Id: abc123(4) MCP:    - 读取 session    - 找到用户上下文    - 找到已完成的 OAuth 状态    - 找到前一步协商结果(5) MCP → ChatGPT    返回订单列表    可同时返回 streaming progress / notification--------------------------------(6) ChatGPT → MCP    tool: refundOrder(order_id=123)    headers:      Mcp-Session-Id: abc123(7) MCP:    - 复用当前 session    - 不需要重新识别用户    - 不需要重新建立上下文    - 可直接执行退款逻辑(8) MCP → ChatGPT    返回退款结果
复制代码
特点

优点


  • 自动维护上下文
  • 适合多步骤流程
  • 适合复杂 Agent 协作
  • 支持流式返回
  • 支持服务端通知
  • 适合长任务
  • 可以复用授权、能力协商和工具状态
缺点


  • 需要额外的 session 存储
  • 部署复杂度更高
  • 可能需要 Redis、内存态、sticky session 或共享状态层
  • 横向扩展时要考虑会话一致性
无状态流程(Stateless MCP)

流程图
  1. (1) ChatGPT → MCP    tool: getOrders    headers:      Authorization: Bearer xxx    body:      user_id = 123(2) MCP:    - 每次重新解析 token    - 每次重新识别用户    - 每次重新决定本次调用逻辑(3) MCP → ChatGPT    返回订单列表--------------------------------(4) ChatGPT → MCP    tool: refundOrder    headers:      Authorization: Bearer xxx    body:      order_id = 123      user_id = 123(5) MCP:    - 再次校验 token    - 再次读取用户    - 再次独立执行退款(6) MCP → ChatGPT    返回退款结果
复制代码
特点

优点


  • 架构简单
  • 易于部署
  • 天然适合 serverless / edge
  • 水平扩展简单
  • 不需要 session 存储
  • 更接近普通 HTTP API 模式
缺点


  • 每次请求都要重复传递上下文
  • 每次都要重新解析认证
  • 不适合复杂的多轮流程
  • 不适合长时间任务协作
  • 服务端通知和流式体验通常更弱
  • 客户端负担更重
核心差异总结

维度有状态 MCP无状态 MCP会话有 session_id无上下文Server 保存Client 每次传OAuth / Token可缓存、可复用每次解析初始化协商协商后复用更偏每次独立处理Streaming强支持较弱服务端通知更自然较难多步骤流程非常适合实现麻烦长任务适合不理想部署难度较高较低扩展性需要考虑状态一致性更容易水平扩展典型场景Agent / Workflow / ChatGPT App简单 API 包装为什么 MCP 一开始看起来是“有状态”的

MCP 一开始看起来偏有状态,主要是因为它的设计目标不是普通 API,而是持续协作
1)它需要维护会话上下文

MCP 不只是“调用一个工具然后结束”。
它经常需要知道:

  • 当前连接属于谁
  • 前面做过哪些能力协商
  • 当前执行到了哪一步
  • 是否已经完成授权
  • 是否已有可复用的上下文数据
如果这些内容全部不存,很多复杂交互都要由客户端重复传递,协议会变得很笨重。
2)它要支持服务端主动发消息

MCP 不只是等客户端请求再响应。
它常常还需要:

  • 推送进度
  • 推送通知
  • 推送后续事件
  • 在长任务中持续输出
这种模式天然更适合依赖会话,而不是每次都从零开始。
3)初始化协商本身就是状态

MCP 在建立连接后,往往要先完成初始化,例如:

  • 支持哪些能力
  • 支持哪些工具
  • 支持哪些资源
  • 支持哪些传输方式
  • 授权与认证如何协同
协商完成以后,后续请求可以直接复用,不需要每次重来。
这本质上就是一种状态。
4)授权和资源访问也更适合绑定会话

在真实应用里,OAuth、资源访问权限、用户身份等信息,通常更容易和某个会话绑定。
这样可以减少:

  • 重复校验
  • 重复交换
  • 重复构造上下文
也能让复杂场景更顺畅。
为什么现在又支持“无状态”

虽然 MCP 最初更偏有状态,但现代基础设施更偏爱无状态:

  • API Gateway
  • Serverless
  • Edge Runtime
  • Auto Scaling
  • 多副本部署
  • 云原生网关
这些环境都更适合无状态服务
所以,MCP 在工程实践上也开始越来越支持:

  • Stateless HTTP
  • 纯请求驱动模式
  • 更轻量的服务部署方式
这意味着:
MCP 的“协议思想”偏会话,
但 MCP 的“工程落地”可以无状态。
怎么选:有状态还是无状态

适合用有状态 MCP 的场景

如果你在做下面这些,优先考虑 Stateful

  • AI Agent 工具链
  • 多步骤业务流程
  • ChatGPT App 的复杂交互
  • 文件处理、任务编排
  • 长任务执行
  • Streaming 输出
  • 进度通知
  • 服务端主动推送
  • 需要恢复中间执行状态
典型例子


  • “先查订单,再选择订单,再退款”
  • “上传文件后做异步解析,再持续返回处理进度”
  • “执行复杂工作流,中途调用多个工具”
  • “一个会话里连续使用多个资源和工具”
适合用无状态 MCP 的场景

如果你在做下面这些,优先考虑 Stateless

  • 简单查询 API
  • 单轮调用即完成
  • 数据读取类工具
  • 高并发简单接口
  • 想快速部署到 serverless 或 edge
  • 不需要通知、不需要长流程
典型例子


  • “查天气”
  • “查订单列表”
  • “获取用户资料”
  • “查询库存”
  • “搜索文档摘要”
所以个人优选 无状态的mcp,毕竟基础设施简单,不过 ts的版本对于stateless支持还有问题 issue:Implement SEP-1442: Make MCP Stateless
而 c# 版本只需一句:
  1. services.AddMcpServer().WithHttpTransport(options =>{    options.Stateless = true;})
复制代码
当然目前还缺乏 mcp apps 的封装,不过基础还是比较简单,手写也能玩
c# mcp apps 怎么做

直接按照下面来就行,安装 以下库
  1.                                        
复制代码
Program.cs
  1. var builder = WebApplication.CreateBuilder(args);var services = builder.Services;services.AddControllers();if (builder.Environment.IsDevelopmentOrDev()){    services.AddOpenApi();    services.AddEndpointsApiExplorer();    services.AddSwaggerGen();}ervices.AddMcpServer().WithHttpTransport(options =>{    options.Stateless = true;})    .WithResources().WithTools(); //mcp 定义var app = builder.Build();if (app.Environment.IsDevelopmentOrDev()){    app.MapOpenApi();    app.UseSwagger();    app.UseSwaggerUI();}app.UseCors(c => c.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());app.MapControllers();app.UseHttpMetrics();app.MapMetrics();app.UseMiddleware(); // 授权验证app.MapMcp("mcp");app.Run();
复制代码
WidgetResource
  1. [McpServerResourceType]public class WidgetResource{    private readonly bool isDevelopment;    private readonly IFileInfo file;    public WidgetResource()    {        this.file = new PhysicalFileProvider(Path.Combine(environment.ContentRootPath, "wwwroot")).GetFileInfo("/index.html");  // 对,ui 是 html, 你以为只用 c# ,做梦吧,梦里什么都有    }    [McpServerResource(MimeType = "text/html;profile=mcp-app", Name = "xxx-app", Title = "xxx-app", UriTemplate = "ui://widget/app.html")]    [McpMeta("openai/widgetPrefersBorder", false)]    [McpMeta("openai/widgetDomain", "xxxx")]    [McpMeta("openai/outputTemplate", "ui://widget/app.html")]    [McpMeta("openai/widgetAccessible", true)]    [McpMeta("openai/widgetCSP", """        {                    "connect_domains": [                        "https://xxx.com"                    ],                    "resource_domains": [                    ]                }        """)]    public async ValueTask Dashboard()    {        using var s = file.CreateReadStream();        return await s.ReadToEndAsync();    }}
复制代码
WigetTool
  1. [McpServerToolType]public class WigetTool{    [AppAuth]    [McpServerTool(Name = "app"), Description(@"xxxx mcp app.")]    [McpMeta("openai/outputTemplate", "ui://widget/app.html")]    [McpMeta("openai/toolInvocation/invoking", "Loading app...")]    [McpMeta("openai/toolInvocation/invoked", "App loaded")]    [McpMeta("openai/widgetAccessible", true)]    [McpMeta("ui", JsonValue = """        {            "resourceUri":"ui://widget/app.html"        }        """)]    public static string GetApp()    {        return $"Loading";    }}
复制代码
UI package.json
  1. {  "name": "ui",  "version": "1.0.0",  "description": "",  "main": "index.js",  "scripts": {    "watch": "cross-env INPUT=index.html vite build --watch"  },  "keywords": [],  "author": "",  "license": "ISC",  "type": "commonjs",  "dependencies": {    "@modelcontextprotocol/ext-apps": "^1.2.2",    "@modelcontextprotocol/sdk": "^1.27.1",    "vue": "^3.5.30",    "zod": "^4.3.6"  },  "devDependencies": {    "@tailwindcss/vite": "^4.2.1",    "@types/cors": "^2.8.19",    "@types/express": "^5.0.6",    "@types/node": "^25.5.0",    "@vitejs/plugin-vue": "^6.0.5",    "concurrently": "^9.2.1",    "cross-env": "^10.1.0",    "tailwindcss": "^4.2.1",    "ts-to-zod": "^5.1.0",    "tsx": "^4.21.0",    "typescript": "^5.9.3",    "vite": "^8.0.0",    "vite-plugin-singlefile": "^2.3.2"  }}
复制代码
UI vite.config.ts
  1. import { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";import { viteSingleFile } from "vite-plugin-singlefile";import tailwindcss from '@tailwindcss/vite'const INPUT = process.env.INPUT;if (!INPUT) {  throw new Error("INPUT environment variable is not set");}const isDevelopment = process.env.NODE_ENV === "development";export default defineConfig({  plugins: [vue(),tailwindcss(), viteSingleFile()],  build: {    sourcemap: isDevelopment ? "inline" : undefined,    cssMinify: !isDevelopment,    minify: !isDevelopment,    rollupOptions: {      input: INPUT,    },    outDir: "../mcp/src/MCPServer/wwwroot",    emptyOutDir: false,  },});
复制代码
UI index.html
  1.         Get Time App   
复制代码
UI mcp-app.ts
  1.         Get Time App   
复制代码
UI app.vue
  1.             [size=6]        Vue 3 + Tailwind CSS 示例      [/size]
  2.                     用户名                            提交                  欢迎, {{ username }}!
  3.             Watch activity in the DevTools console!
  4.           [b]Server Time:[/b] {{ serverTime }}
  5.       Get Server Time                    Send Message                    Send Log                    Open Link      
复制代码
基础只要这样,你可以在本地启动 ui 和 mcp 服务, 然后在 MCP Inspector 你可以非常方便测试
效果比如下图

mcp apps 授权

其实mcp apps 就是支持的  OAuth 2.1, 所以只要你搭建 OAuth 2.1标准的服务就行
当然csharp 这里没有现成的,相关还得我们自己来
首先 暴露元数据
这里方便演示,就直接写死了,实际当然你们得调整,(不想古法编程,就让ai 小龙虾动手嘛)
  1. [Route("/mcp/.well-known")][Route("/.well-known")][ApiController]public class WellKnownController : ControllerBase{    [HttpGet("oauth-protected-resource")]    [HttpGet("oauth-protected-resource/mcp")]    public object GetOAuthProtectedResource()    {        return new        {            resource = "https://xxxxmcp",            authorization_servers = "https://xxxxmcp",            scopes_supported = new string[] { "xxxxmcp" }        };    }    [HttpGet("oauth-authorization-server")]    public object GetOAuthServer()    {        return new        {            issuer = "https://xxxxmcp",            authorization_endpoint = "https://xxxxmcp/oauth/authorize",            token_endpoint = "https://xxxxmcp/oauth/token",            registration_endpoint = "http://localhost:5048/mcp/.well-known/oauth-client-registration",            response_types_supported = new string[] { "code" },            grant_types_supported = new string[] { "authorization_code", "refresh_token" },            subject_types_supported = new string[] { "public" },            id_token_signing_alg_values_supported = new string[] { "RS256" },            scopes_supported = new string[] { "xxxxmcp" },        };    }    [HttpGet("oauth-client-registration")]    public object GetOAuthClientRegistration()    {        return new        {            client_id = "xxx",            client_secret = "xxx",            client_id_issued_at = DateTime.UtcNow.ToUnixTimestamp() / 1000,            client_secret_expires_at = 0,        };    }}
复制代码
最重要是这里Middleware 要按mcp apps 标准通知 mcp client 是需要验证的
  1. public class AppAuthorizationMiddleware : IMiddleware{    private readonly IRestApiClientFactory clientFactory;    public AppAuthorizationMiddleware(IRestApiClientFactory clientFactory)    {        this.clientFactory = clientFactory;    }    public async Task InvokeAsync(HttpContext context, RequestDelegate next)    {        if (context.Request.Path.StartsWithSegments("/mcp") && !context.Request.Path.StartsWithSegments("/mcp/.well-known"))        {            var h = context.Request.Headers.Authorization.ToString();            if (h.StartsWith("Bearer "))            {                h = h.Substring(7);            }            var invalid = h.IsNullOrEmpty();            //实际验证,比如jwt, 这里就不写了            if (invalid)            {                context.Response.Headers.WWWAuthenticate = $"Bearer resource_metadata=http://localhost:5048/mcp/.well-known/oauth-protected-resource";                context.Response.StatusCode = 401;                await context.Response.WriteAsJsonAsync(new { error = "invalid_token", error_description = "Authorization required or access token is invalid" });                return;            }        }        await next(context);    }}
复制代码
这样就够了,其用户数据权限什么的就是大家自己的业务逻辑了

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册
发帖

0

粉丝关注

20

主题发布

板块介绍填写区域,请于后台编辑