惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
爱范儿
爱范儿
J
Java Code Geeks
L
LangChain Blog
V
V2EX
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
博客园 - Franky
Microsoft Azure Blog
Microsoft Azure Blog
Jina AI
Jina AI
Blog — PlanetScale
Blog — PlanetScale
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Cloudflare Blog
博客园 - 司徒正美
B
Blog
G
Google Developers Blog
Stack Overflow Blog
Stack Overflow Blog
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
Engineering at Meta
Engineering at Meta
MyScale Blog
MyScale Blog
有赞技术团队
有赞技术团队
Hugging Face - Blog
Hugging Face - Blog

博客园 - victor.x.qu

用 runtime-async 写一个支持 async/await 的轻量脚本引擎 MCP Tasks 扩展 MCP 企业托管授权(Enterprise-Managed Authorization) 从状态机到运行时:聊聊 .NET 11 的 Runtime Async 和老 Async/Await 到底差在哪 春节期间浅试了 ai 视频生成 举个栗子:做个AI Gateway demo 2025个人总结 实现 json path 来评估函数式解析器的损耗 VKProxy 集成 OpenTelemetry c# ACME client (补充) c# ACME client 留个VKProxy性能测试记录 VKProxy新增一些功能 http流量镜像 VKProxy新增CORS设置和http响应缓存 VKProxy新增速率限制功能 VKProxy已提供命令行工具,镜像和简单的ui 已经在为VKProxy写UI配置站点和文档了 莫想到有一天得重新写个 etcd client 如何使用 websocket 完成 socks5 网络穿透 如何基于 Kestrel 实现 socks5 代理 记录一下 简单udp和sni 代理 done 终于写完轮子一部分:tcp代理 了,记录一下
如何用c# 做 mcp/ChatGPT app
victor.x.qu · 2026-03-21 · via 博客园 - victor.x.qu

什么是 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)

┌────────────────────┐
│     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)

┌────────────────────┐
│     ChatGPT        │
│   (MCP Client)     │
└────────┬───────────┘
         │ 每次请求都完整、自包含
         ▼
┌──────────────────────────────┐
│     MCP Server (Stateless)   │
│                              │
│  No session storage          │
│  No remembered context       │
│                              │
│  Tools / Resources / Logic   │
└────────┬─────────────────────┘
         │
         ▼
   Backend APIs / DB

核心: Server 不保留状态。
每次请求都必须把本次执行所需的信息一起带上,例如:

  • Authorization
  • 用户标识
  • 当前参数
  • 回调信息
  • 本轮调用上下文

有状态流程(Stateful MCP)

流程图

(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) 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# 版本只需一句:

services.AddMcpServer().WithHttpTransport(options =>
{
    options.Stateless = true;
})

当然目前还缺乏 mcp apps 的封装,不过基础还是比较简单,手写也能玩

c# mcp apps 怎么做

直接按照下面来就行,安装 以下库

<ItemGroup>
	<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.1.0" />
		<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
		<PackageReference Include="Microsoft.OpenApi" Version="2.7.0" />
</ItemGroup>

Program.cs

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<WidgetResource>().WithTools<WigetTool>(); //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<AppAuthorizationMiddleware>(); // 授权验证
app.MapMcp("mcp");

app.Run();

WidgetResource

[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<string> Dashboard()
    {
        using var s = file.CreateReadStream();
        return await s.ReadToEndAsync();
    }
}

WigetTool

[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

{
  "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

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

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="color-scheme" content="light dark">
  <title>Get Time App</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/mcp-app.ts"></script>
</body>
</html>

UI mcp-app.ts

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="color-scheme" content="light dark">
  <title>Get Time App</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/mcp-app.ts"></script>
</body>
</html>

UI app.vue

<script setup lang="ts">
import { ref, onMounted, watchEffect } from "vue";
import {
  App,
  applyDocumentTheme,
  applyHostFonts,
  applyHostStyleVariables,
  type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";

function extractTime(result: CallToolResult): string {
  const { text } = result.content?.find((c) => c.type === "text")!;
  return text;
}


const app = ref<App | null>(null);
const hostContext = ref<McpUiHostContext | undefined>();
const serverTime = ref("Loading...");
const messageText = ref("This is a messsssage text.");
const logText = ref("This is log text.");
const linkUrl = ref("https://modelcontextprotocol.io/");

// Apply host styles reactively when hostContext changes
watchEffect(() => {
  const ctx = hostContext.value;
  if (ctx?.theme) {
    applyDocumentTheme(ctx.theme);
  }
  if (ctx?.styles?.variables) {
    applyHostStyleVariables(ctx.styles.variables);
  }
  if (ctx?.styles?.css?.fonts) {
    applyHostFonts(ctx.styles.css.fonts);
  }
});

onMounted(async () => {
  const instance = new App({ name: "Sellingpilot App", version: "1.0.0" });

  instance.ontoolinput = (params) => {
    console.info("Received tool call input:", params);
  };

  instance.ontoolresult = (result) => {
    console.info("Received tool call result:", result);
    serverTime.value = extractTime(result);
  };

  instance.ontoolcancelled = (params) => {
    console.info("Tool call cancelled:", params.reason);
  };

  instance.onerror = console.error;

  instance.onhostcontextchanged = (params) => {
    hostContext.value = { ...hostContext.value, ...params };
  };

  await instance.connect();
  app.value = instance;
  hostContext.value = instance.getHostContext();
});

async function handleGetTime() {
  if (!app.value) return;
  try {
    console.info("Calling get-time tool...");
    const result = await app.value.callServerTool({ name: "get-time", arguments: {} });
    console.info("get-time result:", result);
    serverTime.value = extractTime(result);
  } catch (e) {
    console.error(e);
    serverTime.value = "[ERROR]";
  }
}

async function handleSendMessage() {
  if (!app.value) return;
  const signal = AbortSignal.timeout(5000);
  try {
    console.info("Sending message text to Host:", messageText.value);
    const { isError } = await app.value.sendMessage(
      { role: "user", content: [{ type: "text", text: messageText.value }] },
      { signal },
    );
    console.info("Message", isError ? "rejected" : "accepted");
  } catch (e) {
    console.error("Message send error:", signal.aborted ? "timed out" : e);
  }
}

async function handleSendLog() {
  if (!app.value) return;
  console.info("Sending log text to Host:", logText.value);
  await app.value.sendLog({ level: "info", data: logText.value });
}

async function handleOpenLink() {
  if (!app.value) return;
  console.info("Sending open link request to Host:", linkUrl.value);
  const { isError } = await app.value.openLink({ url: linkUrl.value });
  console.info("Open link request", isError ? "rejected" : "accepted");
}
</script>

<template>
  <div class="min-h-screen bg-gray-50 flex items-center justify-center p-6">
    <div class="w-full max-w-md bg-white p-8 rounded-xl shadow-lg">
      <h1 class="text-2xl font-semibold text-center text-gray-800 mb-6">
        Vue 3 + Tailwind CSS 示例
      </h1>

      <!-- 输入框和按钮 -->
      <div class="mb-4">
        <label for="name" class="block text-sm font-medium text-gray-700">用户名</label>
        <input
         type="text"
         id="name"
         v-model="username"
         placeholder="请输入用户名"
         class="mt-2 w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
       />
      </div>

      <button
       @click="submitForm"
       class="w-full bg-blue-500 text-white py-2 rounded-md shadow-md hover:bg-blue-600 transition duration-200"
     >
        提交
      </button>

      <!-- 显示用户名 -->
      <p v-if="submitted" class="mt-4 text-center text-gray-600">欢迎, {{ username }}!</p>
    </div>
  </div>
  <main
    class="main"
    :style="hostContext?.safeAreaInsets && {
      paddingTop: hostContext.safeAreaInsets.top + 'px',
      paddingRight: hostContext.safeAreaInsets.right + 'px',
      paddingBottom: hostContext.safeAreaInsets.bottom + 'px',
      paddingLeft: hostContext.safeAreaInsets.left + 'px',
    }"
  >
    <p class="notice">Watch activity in the DevTools console!</p>

    <div class="action">
      <p><strong>Server Time:</strong> <code class="server-time">{{ serverTime }}</code></p>
      <button @click="handleGetTime">Get Server Time</button>
    </div>

    <div class="action">
      <textarea v-model="messageText"></textarea>
      <button @click="handleSendMessage">Send Message</button>
    </div>

    <div class="action">
      <input type="text" v-model="logText">
      <button @click="handleSendLog">Send Log</button>
    </div>

    <div class="action">
      <input type="url" v-model="linkUrl">
      <button @click="handleOpenLink">Open Link</button>
    </div>
  </main>
</template>

<style scoped>

</style>

基础只要这样,你可以在本地启动 ui 和 mcp 服务, 然后在 MCP Inspector 你可以非常方便测试
效果比如下图

屏幕截图 2026-03-21 145527

mcp apps 授权

其实mcp apps 就是支持的 OAuth 2.1, 所以只要你搭建 OAuth 2.1标准的服务就行
当然csharp 这里没有现成的,相关还得我们自己来

首先 暴露元数据

这里方便演示,就直接写死了,实际当然你们得调整,(不想古法编程,就让ai 小龙虾动手嘛)

[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 是需要验证的

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);
    }
}

这样就够了,其用户数据权限什么的就是大家自己的业务逻辑了