如何用c# 做 mcp/ChatGPT app

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

text 复制代码
┌────────────────────┐
│     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)

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

核心: Server 不保留状态。

每次请求都必须把本次执行所需的信息一起带上,例如:

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

有状态流程(Stateful MCP)

流程图

text 复制代码
(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)

流程图

text 复制代码
(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# 版本只需一句:

csharp 复制代码
services.AddMcpServer().WithHttpTransport(options =>
{
    options.Stateless = true;
})

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

c# mcp apps 怎么做

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

xml 复制代码
<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

csharp 复制代码
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

csharp 复制代码
[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

csharp 复制代码
[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

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

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

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

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

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 你可以非常方便测试 效果比如下图

mcp apps 授权

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

首先 暴露元数据

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

csharp 复制代码
[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 是需要验证的

csharp 复制代码
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);
    }
}

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

相关推荐
人工智能AI技术3 小时前
DeskClaw Windows上线|C#开发AI桌面助手,轻量内核源码解析
人工智能·c#
似水明俊德3 小时前
04-C#.Net-委托和事件-面试题
java·开发语言·面试·c#·.net
光于前裕于后6 小时前
配置钉钉龙虾OpenClaw机器人调用OpenMetadata
机器人·钉钉·数据治理·mcp·openclaw
程序员老乔6 小时前
Java 新纪元 — JDK 25 + Spring Boot 4 全栈实战(二):Valhalla落地,值类型如何让电商DTO内存占用暴跌
java·spring boot·c#
祝大家百事可乐7 小时前
嵌入式——02 数据结构
c++·c#·硬件工程
星野云联AIoT技术洞察7 小时前
2026 年 MCP + MQTT:AI Agent 真正控制 IoT 设备的落地路径
数字孪生·ack·ai agent·物联网平台·agentic·mcp·命令服务
我是唐青枫7 小时前
深入理解 C#.NET TaskScheduler:为什么大量使用 Work-Stealing
c#·.net
唯情于酒9 小时前
net core web api 使用log4net
c#·.net core
SunnyDays10119 小时前
C# 实战:快速查找并高亮 Word 文档中的文字(普通查找 + 正则表达式)
开发语言·c#