抛开官方库,手撸一个轻量级 MCP 服务端 - .NET骚操作 - 博客园

mikel阅读(402)

来源: 抛开官方库,手撸一个轻量级 MCP 服务端 – .NET骚操作 – 博客园

大家好!在昨天的文章 官方文档没告诉你的:通过抓包,深入揭秘MCP协议底层通信 中,我们通过Fiddler工具,像侦探一样,一步步揭开了MCP(Model Context Protocol)在无状态HTTP模式下的神秘面纱。我们搞清楚了它的两步握手、SSE(Server-Sent Events)响应机制以及精巧的两种错误处理方式。

image

然而,仅仅停留在理论分析层面总感觉意犹未尽。更重要的是,当我们审视官方提供的 ModelContextProtocol.AspNetCore 这个NuGet包时(当前版本0.3.0-preview.3),会发现它目前引入了相当多的依赖项:

  • Microsoft.Bcl.Memory (>= 9.0.5)
  • Microsoft.Extensions.AI.Abstractions (>= 9.7.1)
  • Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
  • System.Diagnostics.DiagnosticSource (>= 8.0.1)
  • System.IO.Pipelines (>= 8.0.0)
  • System.Net.ServerSentEvents (>= 10.0.0-preview.4.25258.110)
  • System.Text.Json (>= 8.0.6)
  • System.Threading.Channels (>= 8.0.0)
  • Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
  • ModelContextProtocol.Core (>= 0.3.0-preview.3)

其中,最令人不安的莫过于 System.Net.ServerSentEvents,它竟然是一个 .NET 10 的预览版包!在生产环境中使用预览版包,通常是大忌。

既然我们已经通过抓包掌握了协议的全部细节,那么,何不自己动手,实现一个轻量级、零预览版依赖的MCP服务端呢?这不仅是一次绝佳的学习实践,也能让我们对协议的理解更上一层楼。

今天,我们就来完成这个挑战:不依赖官方服务端库,直接用纯粹的ASP.NET Core代码,实现一个功能完备的MCP服务端。

我们的目标:保持工具定义的简洁性

在动手之前,我们先定一个目标。我们希望定义工具(Tools)的方式能够尽可能地简洁和直观,几乎和昨天的代码保持一致:

using System.ComponentModel;

public class Tools(IHttpContextAccessor http)
{
    [Description("Echoes the message back to the client.")]
    public string Echo(string message) => $"hello {message}";

    [Description("Returns the IP address of the client.")]
    public string EchoIP() => http.HttpContext?.Connection.RemoteIpAddress?.ToString() ?? "Unknown";

    [Description("Counts from 0 to n, reporting progress at each step.")]
    public async Task<int> Count(int n, IProgress<ProgressNotificationValue> progress)
    {
        for (int i = 0; i < n; ++i)
        {
            progress.Report(new ProgressNotificationValue()
            {
                Progress = i,
                Total = n,
                Message = $"Step {i} of {n}",
            });
            await Task.Delay(100);
        }
        return n;
    }

    [Description("Throws an exception for testing purposes.")]
    public string TestThrow()
    {
        throw new Exception("This is a test exception");
    }
}

注意到变化了吗?我们去掉了官方库定义的 [McpServerToolType] 和 [McpServerTool] 特性。取而代之的是一种更符合ASP.NET Core直觉的方式:任何 public 方法都自动成为一个工具,并使用标准的 System.ComponentModel.DescriptionAttribute 来提供工具描述。

理想中的使用方式

我们期望最终的使用方式能像下面这样优雅:

WebApplicationBuilder builder = WebApplication.CreateBuilder();

// 1. 注册原生服务和我们的工具类
builder.Services.AddHttpContextAccessor();
builder.Services.AddTransient<Tools>();

WebApplication app = builder.Build();

// 2. 映射 MCP 端点,自动发现并使用 Tools 类
app.MapMcpEndpoint<Tools>("/");

// 3. 启动应用
app.Run();

是的,你没看错。核心就在于 builder.Services.AddTransient<Tools>(); 和 app.MapMcpEndpoint<Tools>("/"); 这两行。前者负责将我们的工具类注册到依赖注入容器,后者则是我们即将创建的魔法扩展方法,它会自动处理所有MCP协议的细节。

第一步:定义协议的“语言” – DTOs

要实现协议,首先要定义好通信双方所使用的“语言”,也就是数据传输对象(DTOs)。根据昨天的抓包分析,我们用C#的 record 类型来精确描述这些JSON结构。

using System.Text.Json.Serialization;

// --- JSON-RPC Base Structures ---
public record JsonRpcRequest(
    [property: JsonPropertyName("jsonrpc")] string JsonRpc,
    [property: JsonPropertyName("method")] string Method,
    [property: JsonPropertyName("params")] object? Params,
    [property: JsonPropertyName("id")] int? Id
);

public record JsonRpcResponse(
    [property: JsonPropertyName("jsonrpc")] string JsonRpc,
    [property: JsonPropertyName("result")] object? Result,
    [property: JsonPropertyName("error")] object? Error,
    [property: JsonPropertyName("id")] int? Id
);

public record JsonRpcError(
    [property: JsonPropertyName("code")] int Code,
    [property: JsonPropertyName("message")] string Message
);

// --- MCP Specific Payloads ---

// For initialize method
public record InitializeParams(
    [property: JsonPropertyName("protocolVersion")] string ProtocolVersion,
    [property: JsonPropertyName("clientInfo")] ClientInfo ClientInfo
);
public record ClientInfo([property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("version")] string Version);

public record InitializeResult(
    [property: JsonPropertyName("protocolVersion")] string ProtocolVersion,
    [property: JsonPropertyName("capabilities")] ServerCapabilities Capabilities,
    [property: JsonPropertyName("serverInfo")] ClientInfo ServerInfo
);
public record ServerCapabilities([property: JsonPropertyName("tools")] object Tools);


// For tools/call method
public record ToolCallParams(
    [property: JsonPropertyName("name")] string Name,
    [property: JsonPropertyName("arguments")] Dictionary<string, object?> Arguments,
    [property: JsonPropertyName("_meta")] ToolCallMeta? Meta
);
public record ToolCallMeta([property: JsonPropertyName("progressToken")] string ProgressToken);

// For tool call results
public record ToolCallResult(
    [property: JsonPropertyName("content")] List<ContentItem> Content,
    [property: JsonPropertyName("isError")] bool IsError = false
);
public record ContentItem([property: JsonPropertyName("type")] string Type, [property: JsonPropertyName("text")] string Text);

// For tools/list results
public record ToolListResult(
    [property: JsonPropertyName("tools")] List<ToolDefinition> Tools
);

public record ToolDefinition(
    [property: JsonPropertyName("name")] string Name,
    [property: JsonPropertyName("description")] string Description,
    [property: JsonPropertyName("inputSchema")] object InputSchema
);

// For progress notifications
public record ProgressNotification(
    [property: JsonPropertyName("jsonrpc")] string JsonRpc,
    [property: JsonPropertyName("method")] string Method,
    [property: JsonPropertyName("params")] ProgressParams Params
);
public record ProgressParams(
    [property: JsonPropertyName("progressToken")] string ProgressToken,
    [property: JsonPropertyName("progress")] int Progress,
    [property: JsonPropertyName("total")] int Total,
    [property: JsonPropertyName("message")] string Message
);

// This class is for the IProgress<T> interface in our Tools methods
public class ProgressNotificationValue
{
    public int Progress { get; set; }
    public int Total { get; set; }
    public string Message { get; set; } = string.Empty;
}

第二步:打造核心引擎 – McpEndpointExtensions

接下来,就是实现我们魔法的源泉:一个IEndpointRouteBuilder的扩展方法。我们将所有逻辑都封装在一个静态类 McpEndpointExtensions 中。

这个类将负责:

  1. 路由映射:监听指定路径的 POST 和 GET 请求。
  2. 请求分发:根据JSON-RPC请求中的method字段,调用不同的处理函数。
  3. 工具发现与调用:使用反射来查找和执行TTools类中的工具方法。
  4. 响应构建:手动构建符合SSE规范的响应流。
  5. 错误处理:精确复现抓包分析中发现的两种错误模型。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;
using System.ComponentModel;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

public static class McpEndpointExtensions
{
    // JSON-RPC Error Codes from your article's findings
    private const int InvalidParamsErrorCode = -32602; // Invalid params
    private const int MethodNotFoundErrorCode = -32601; // Method not found

    private static readonly JsonSerializerOptions s_jsonOptions = new()
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    };

    /// <summary>
    /// Maps an endpoint that speaks the Model Context Protocol.
    /// </summary>
    public static IEndpointRouteBuilder MapMcpEndpoint<TTools>(this IEndpointRouteBuilder app, string pattern) where TTools : class
    {
        // 预先通过反射发现所有工具方法,并转换为snake_case以匹配MCP命名习惯
        Dictionary<string, MethodInfo> methods = typeof(TTools).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
            .ToDictionary(k => ToSnakeCase(k.Name), v => v);

        app.MapPost(pattern, async (HttpContext context, [FromServices] IServiceProvider sp) =>
        {
            JsonRpcRequest? request = await JsonSerializer.DeserializeAsync<JsonRpcRequest>(context.Request.Body, s_jsonOptions);
            if (request == null)
            {
                context.Response.StatusCode = 400; // Bad Request
                return;
            }

            // 核心:处理不同的MCP方法
            switch (request.Method)
            {
                case "initialize":
                    await HandleInitialize(context, request);
                    break;
                case "notifications/initialized":
                    // 在无状态模式下,这个请求只是一个确认,我们返回与initialize类似的信息
                    await HandleInitialize(context, request);
                    break;
                case "tools/list":
                    await HandleToolList<TTools>(context, request);
                    break;
                case "tools/call":
                    await HandleToolCall<TTools>(context, request, sp, methods);
                    break;
                default:
                    JsonRpcResponse errorResponse = new("2.0", null, new JsonRpcError(MethodNotFoundErrorCode, "Method not found"), request.Id);
                    await WriteSseMessageAsync(context.Response, errorResponse);
                    break;
            }
        });

        // 旧版SDK会发送GET请求,我们明确返回405
        app.MapGet(pattern, context =>
        {
            context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
            context.Response.Headers.Allow = "POST";
            return Task.CompletedTask;
        });

        return app;
    }

    private static string ToSnakeCase(string name)
    {
        if (string.IsNullOrEmpty(name)) return name;
        var sb = new StringBuilder(name.Length);
        for (int i = 0; i < name.Length; i++)
        {
            char c = name[i];
            if (char.IsUpper(c))
            {
                if (sb.Length > 0 && i > 0 && !char.IsUpper(name[i-1])) sb.Append('_');
                sb.Append(char.ToLowerInvariant(c));
            }
            else
            {
                sb.Append(c);
            }
        }
        return sb.ToString();
    }

    private static async Task HandleInitialize(HttpContext context, JsonRpcRequest request)
    {
        // 复用或创建 Session ID
        string sessionId = context.Request.Headers.TryGetValue("Mcp-Session-Id", out StringValues existingSessionId)
            ? existingSessionId.ToString()
            : WebEncoders.Base64UrlEncode(Guid.NewGuid().ToByteArray());

        context.Response.Headers["Mcp-Session-Id"] = sessionId;

        // 构建与抓包一致的响应
        InitializeResult result = new(
            "2025-06-18", // Echo the protocol version
            new ServerCapabilities(new { listChanged = true }), // Mimic the capabilities
            new ClientInfo("PureAspNetCoreMcpServer", "1.0.0")
        );
        JsonRpcResponse response = new("2.0", result, null, request.Id);
        await WriteSseMessageAsync(context.Response, response);
    }

    private static async Task HandleToolList<TTools>(HttpContext context, JsonRpcRequest request) where TTools : class
    {
        EchoSessionId(context);

        List<ToolDefinition> toolDefs = [];
        MethodInfo[] toolMethods = typeof(TTools).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);

        foreach (MethodInfo method in toolMethods)
        {
            string description = method.GetCustomAttribute<DescriptionAttribute>()?.Description ?? "No description.";

            // 简化的动态Schema生成
            Dictionary<string, object> properties = [];
            List<string> required = [];
            foreach (ParameterInfo param in method.GetParameters())
            {
                if (param.ParameterType == typeof(IProgress<ProgressNotificationValue>)) continue; // 忽略进度报告参数
                properties[param.Name!] = new { type = GetJsonType(param.ParameterType) };
                if (!param.IsOptional)
                {
                    required.Add(param.Name!);
                }
            }
            var schema = new { type = "object", properties, required };
            toolDefs.Add(new ToolDefinition(ToSnakeCase(method.Name), description, schema));
        }

        ToolListResult result = new(toolDefs);
        JsonRpcResponse response = new("2.0", result, null, request.Id);
        await WriteSseMessageAsync(context.Response, response);
    }

    private static async Task HandleToolCall<TTools>(HttpContext context, JsonRpcRequest request, IServiceProvider sp, Dictionary<string, MethodInfo> methods) where TTools : class
    {
        EchoSessionId(context);

        ToolCallParams? toolCallParams = JsonSerializer.Deserialize<ToolCallParams>(JsonSerializer.Serialize(request.Params, s_jsonOptions), s_jsonOptions);
        if (toolCallParams == null) return;

        string toolName = toolCallParams.Name;
        methods.TryGetValue(toolName, out MethodInfo? method);

        // 场景1: 调用不存在的工具 -> 返回标准JSON-RPC错误
        if (method == null)
        {
            JsonRpcError error = new(InvalidParamsErrorCode, $"Unknown tool: '{toolName}'");
            JsonRpcResponse response = new("2.0", null, error, request.Id);
            await WriteSseMessageAsync(context.Response, response);
            return;
        }

        // 使用DI容器创建工具类的实例
        using IServiceScope scope = sp.CreateScope();
        TTools toolInstance = scope.ServiceProvider.GetRequiredService<TTools>();

        object? resultValue;
        bool isError = false;

        try
        {
            // 通过反射准备方法参数
            ParameterInfo[] methodParams = method.GetParameters();
            object?[] args = new object?[methodParams.Length];
            for (int i = 0; i < methodParams.Length; i++)
            {
                ParameterInfo p = methodParams[i];
                if (p.ParameterType == typeof(IProgress<ProgressNotificationValue>))
                {
                    // 创建一个IProgress<T>的实现,它会将进度作为SSE消息发回客户端
                    args[i] = new ProgressReporter(context.Response, toolCallParams.Meta!.ProgressToken);
                }
                else if (toolCallParams.Arguments.TryGetValue(p.Name!, out object? argValue) && argValue is JsonElement element)
                {
                    args[i] = element.Deserialize(p.ParameterType, s_jsonOptions);
                }
                else if (p.IsOptional)
                {
                    args[i] = p.DefaultValue;
                }
                else
                {
                     // 场景2a: 缺少必要参数 -> 抛出异常,进入catch块
                    throw new TargetParameterCountException($"Tool '{toolName}' requires parameter '{p.Name}' but it was not provided.");
                }
            }

            object? invokeResult = method.Invoke(toolInstance, args);

            // 处理异步方法
            if (invokeResult is Task task)
            {
                await task;
                resultValue = task.GetType().IsGenericType ? task.GetType().GetProperty("Result")?.GetValue(task) : null;
            }
            else
            {
                resultValue = invokeResult;
            }
        }
        // 场景2b: 工具执行时内部抛出异常 -> isError: true
        catch (Exception ex)
        {
            isError = true;
            // 将异常信息包装在result中,而不是顶层error
            resultValue = $"An error occurred invoking '{toolName}'. Details: {ex.InnerException?.Message ?? ex.Message}";
        }

        List<ContentItem> content = [new("text", resultValue?.ToString() ?? string.Empty)];
        ToolCallResult result = new(content, isError);
        JsonRpcResponse finalResponse = new("2.0", result, null, request.Id);
        await WriteSseMessageAsync(context.Response, finalResponse);
    }

    // 手动实现SSE消息写入,告别预览版包
    private static async Task WriteSseMessageAsync(HttpResponse response, object data)
    {
        if (!response.Headers.ContainsKey("Content-Type"))
        {
            response.ContentType = "text/event-stream";
            response.Headers.CacheControl = "no-cache,no-store";
            response.Headers.ContentEncoding = "identity";
            response.Headers.KeepAlive = "true";
        }

        string json = JsonSerializer.Serialize(data, s_jsonOptions);
        string message = $"event: message\ndata: {json}\n\n";
        await response.WriteAsync(message);
        await response.Body.FlushAsync();
    }

    private static void EchoSessionId(HttpContext context)
    {
        if (context.Request.Headers.TryGetValue("Mcp-Session-Id", out StringValues sessionId))
        {
            context.Response.Headers["Mcp-Session-Id"] = sessionId;
        }
    }

    private static string GetJsonType(Type type) => Type.GetTypeCode(type) switch
    {
        TypeCode.String => "string",
        TypeCode.Int32 or TypeCode.Int64 or TypeCode.Int16 or TypeCode.UInt32 => "integer",
        TypeCode.Double or TypeCode.Single or TypeCode.Decimal => "number",
        TypeCode.Boolean => "boolean",
        _ => "object"
    };

    // 专门用于处理进度报告的辅助类
    private class ProgressReporter(HttpResponse response, string token) : IProgress<ProgressNotificationValue>
    {
        public void Report(ProgressNotificationValue value)
        {
            ProgressParams progressParams = new(token, value.Progress, value.Total, value.Message);
            ProgressNotification notification = new("2.0", "notifications/progress", progressParams);
            // 警告: 在同步方法中调用异步代码,在真实生产环境中需要更优雅的处理
            WriteSseMessageAsync(response, notification).GetAwaiter().GetResult();
        }
    }
}

完整代码已备好!

为了方便大家动手实践,我已经将上述所有可直接运行的示例代码上传到了 GitHub Gist。您可以通过以下链接访问:

该Gist中包含了两个文件:

  • mcp-server-raw.linq: 我们刚刚从零开始构建的轻量级MCP服务端。
  • mcp-client.linq: 用于测试的客户端。

这两个文件都可以直接在最新版的 LINQPad 中打开并运行,让您能够立即体验和调试,如果您访问 Github Gist 有困难,则可以访问这个备用地址:https://github.com/sdcb/blog-data/tree/master/2025

bg

第三步:见证奇迹的时刻

现在,我们所有的准备工作都已就绪。我们可以用和昨天一模一样的客户端代码来测试我们的新服务端了:

// 客户端代码完全不变!
var clientTransport = new SseClientTransport(new SseClientTransportOptions()
{
    Name = "MyServer",
    Endpoint = new Uri("http://localhost:5000"), // 注意端口可能不同
});

var client = await McpClientFactory.CreateAsync(clientTransport);

// 1. 列出工具
(await client.ListToolsAsync()).Select(x => new { x.Name, Desc = JsonObject.Parse(x.JsonSchema.ToString()) }).Dump();

// 2. 调用简单工具
(await client.CallToolAsync(
    "echo",
    new Dictionary<string, object?>() { ["message"] = ".NET is awesome!" },
    cancellationToken: CancellationToken.None)).Dump();

// 3. 调用带进度的工具
(await client.CallToolAsync(
    "count",
    new Dictionary<string, object?>() { ["n"] = 5 },
    new Reporter(),
    cancellationToken: CancellationToken.None)).Dump();
    
// 4. 调用会抛出异常的工具
(await client.CallToolAsync("test_throw", cancellationToken: CancellationToken.None)).Dump();

// 5. 调用不存在的工具
(await client.CallToolAsync("not-existing-tool", cancellationToken: CancellationToken.None)).Dump();

// ... Reporter class as before ...

启动我们的新服务端,再运行客户端代码。打开抓包工具,你会发现,所有HTTP请求和SSE响应的格式、内容和行为,都与昨天分析的官方库实现完全一致!我们成功了!

对错误处理的进一步思考

值得一提的是,昨天的文章没有深入探讨参数错误的情况。比如 count 工具需要一个名为 n 的 int 类型参数,如果客户端错误地传递了一个 n2 参数,会发生什么?

在我今天实现的 HandleToolCall 方法中,参数匹配逻辑会因为找不到名为 n 的键而抛出 TargetParameterCountException。这个异常会被 try-catch 块捕获,然后和 test_throw 的情况一样,返回一个调用“成功”(HTTP 200)、但在 result 载荷中包含 "isError": true 和详细错误信息的响应。这恰好证明了MCP这种错误处理设计的健壮性:它能统一处理业务逻辑层面(工具内部异常)和参数绑定层面(调用约定不匹配)的多种失败情况。

总结

通过本次实践,我们不仅重温了MCP协议的通信原理,更重要的是,我们亲手实现了一个轻量级、无预览版依赖的MCP服务端。这次旅程的核心收获是:

  1. 协议是根基:一旦深刻理解了协议本身,即使没有官方SDK,我们也能在任何支持HTTP的环境中实现它。
  2. 化繁为简:我们用一个扩展方法和一些辅助类,就替代了官方库及其繁杂的依赖,代码清晰且易于掌控。
  3. 反射与元编程的威力:通过巧妙运用反射,我们实现了工具的自动发现和动态调用,大大提高了代码的灵活性和可扩展性。
  4. 知其然,知其所以然:现在,我们不仅知道MCP如何工作,更通过自己动手理解了它为何如此设计,比如两步握手、SSE流式响应以及分层的错误处理机制。

希望本文能帮助你彻底搞懂并掌握MCP协议的实现细节。现在,你拥有了完全控制MCP通信的能力,无论是进行二次开发、跨语言实现,还是仅仅为了满足那份技术探索的好奇心。


感谢您的阅读,如果您有任何问题或想法,欢迎在评论区留言讨论。

也欢迎加入我们的 .NET骚操作 QQ群一起探讨:495782587

官方文档没告诉你的:通过抓包,深入揭秘MCP协议底层通信 - .NET骚操作 - 博客园

mikel阅读(225)

来源: 官方文档没告诉你的:通过抓包,深入揭秘MCP协议底层通信 – .NET骚操作 – 博客园

大家好,今天我们来深入探讨一个很有意思的话题——MCP(Model Context Protocol)。

MCP 是Anthropic发起的一种开放协议,旨在标准化应用程序向大型语言模型(LLM)提供上下文的方式。我们可以把 MCP 想象成 AI 应用领域的 USB-C 接口。正如 USB-C 为你的设备与各种外设和配件的连接提供了标准化方式一样,MCP 也为 AI 模型与不同数据源和工具的连接提供了标准化的方法。

然而,MCP官网只模糊地提到它是基于 JSON-RPC 2.0 的协议,并提供了包括 C# 在内的八种语言的SDK。但对于其底层的HTTP请求和响应格式,官方文档并未给出清晰的描述,这让许多想要深入了解或自行实现的开发者感到困惑。

本文将通过一个具体的 C# 实例,结合抓包数据,一步步揭开 MCP 协议在 HTTP 层面上的神秘面纱。

image

准备工作:示例代码

为了抓包和演示,我们首先需要一个客户端和一个服务端。这里我们使用的是 ModelContextProtocol 0.3.0-preview.3 版本的 NuGet 包。

客户端 (Client)

客户端代码负责发起连接、列出可用工具并调用它们。

// 需要安装NuGet包:ModelContextProtocol 0.3.0-preview.3

var clientTransport = new SseClientTransport(new SseClientTransportOptions()
{
    Name = "MyServer",
    Endpoint = new Uri("http://localhost:5000/"),
});

var client = await McpClientFactory.CreateAsync(clientTransport);

// Print the list of tools available from the server.
(await client.ListToolsAsync()).Select(x => new { x.Name, Desc = JsonObject.Parse(x.JsonSchema.ToString()) }).Dump();

// Execute a tool (this would normally be driven by LLM tool invocations).
(await client.CallToolAsync(
    "echo",
    new Dictionary<string, object?>() { ["message"] = ".NET is awesome!" },
    cancellationToken: CancellationToken.None)).Dump();

(await client.CallToolAsync(
    "count",
    new Dictionary<string, object?>() { ["n"] = 5 },
    new Reporter(),
    cancellationToken: CancellationToken.None)).Dump();

(await client.CallToolAsync("test_throw", cancellationToken: CancellationToken.None)).Dump();

(await client.CallToolAsync("not-existing-tool", cancellationToken: CancellationToken.None)).Dump();

public class Reporter : IProgress<ProgressNotificationValue>
{
    public void Report(ProgressNotificationValue value)
    {
        value.Dump();
    }
}

服务端 (Server)

服务端代码定义了几个可供客户端调用的工具(Tool),并处理 MCP 请求。

// 需要安装NuGet包:ModelContextProtocol.AspNetCore 0.3.0-preview.3
var builder = WebApplication.CreateBuilder();

builder.Logging.AddConsole(consoleLogOptions =>
{
    // Configure all logs to go to stderr
    consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services
    .AddHttpContextAccessor()
    .AddMcpServer()
    .WithHttpTransport(c => c.Stateless = true) // 注意这里!
    .WithTools<Tools>();
var app = builder.Build();

app.MapMcp();
await app.RunAsync(QueryCancelToken);

[McpServerToolType]
public class Tools(IHttpContextAccessor http)
{
    [McpServerTool, Description("Echoes the message back to the client.")]
    public string Echo(string message) => $"hello {message}";

    [McpServerTool, Description("Returns the IP address of the client.")]
    public string EchoIP() => http.HttpContext?.Connection.RemoteIpAddress?.ToString() ?? "Unknown";

    [McpServerTool, Description("Counts from 0 to n, reporting progress at each step.")]
    public async Task<int> Count(int n, IProgress<ProgressNotificationValue> progress)
    {
        for (int i = 0; i < n; ++i)
        {
            progress.Report(new ProgressNotificationValue()
            {
                Progress = i,
                Total = n,
                Message = $"Step {i} of {n}",
            });
            await Task.Delay(100);
        }
        return n;
    }

    [McpServerTool, Description("Throws an exception for testing purposes.")]
    public string TestThrow()
    {
        throw new Exception("This is a test exception");
    }
}

特别注意:在我的服务端示例中,我明确指定了 .WithHttpTransport(c => c.Stateless = true)。这代表我使用的是无状态的HTTP传输方式。MCP目前默认是有状态的,如果使用有状态模式,具体的请求和响应格式会略有不同。本文的分析全部基于此处的无状态模式。

第一部分:初始化握手

MCP的连接始于一个分为两步的初始化过程,我们可以称之为“协商”与“确认”。

1. 协商 (Negotiation)

客户端首先向服务器发送一个initialize方法的JSON-RPC请求。

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8

{
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": {
      "name": "LINQPad.ScriptHost",
      "version": "1.0.0.0"
    }
  },
  "id": 1,
  "jsonrpc": "2.0"
}

这个请求告诉服务器:客户端期望使用2025-06-18版本的协议,并附上了自己的身份信息。

服务器收到后,会返回一个 Server-Sent Events (SSE) 响应。这个响应中包含一个关键的HTTP头 Mcp-Session-Id,以及对初始化请求的回复。

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 01:57:01 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7__UKHVt_9PM30T9KfWqDcHrSQUy3f34bIXzMKW-r2xhMrKclIQijzdY8FIWAAMLXnHVpWepSgNmZ02LKSIgsThMwffivlsALrlt_5PBExlLXRZo59M7NL3sDmWf22zTLPGymVcJHKk_lPOvSxV5ClxspnQbKLx-XgqPCAU6yt6D2E060A-fJoZ_vYNqpYe08bXkTvOdsvCrbweWBcsdL1cABx5jwfypX1CuZkcbuTUA

event: message
data: {"result":{"protocolVersion":"2025-06-18","capabilities":{"logging":{},"tools":{"listChanged":true}},"serverInfo":{"name":"LINQPad.ScriptHost","version":"1.0.0.0"}},"id":1,"jsonrpc":"2.0"}

从响应中可以看到,服务器同意使用2025-06-18协议版本,并返回了自己的能力(capabilities)。最重要的是,它提供了一个唯一的会话ID Mcp-Session-Id,这个ID将用于后续的所有通信。

2. 确认 (Confirmation)

拿到会话ID后,客户端会发送第二个请求,这次是notifications/initialized通知,用于确认初始化。

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7__UKHVt_9PM30T9KfWqDcHrSQUy3f34bIXzMKW-r2xhMrKclIQijzdY8FIWAAMLXnHVpWepSgNmZ02LKSIgsThMwffivlsALrlt_5PBExlLXRZo59M7NL3sDmWf22zTLPGymVcJHKk_lPOvSxV5ClxspnQbKLx-XgqPCAU6yt6D2E060A-fJoZ_vYNqpYe08bXkTvOdsvCrbweWBcsdL1cABx5jwfypX1CuZkcbuTUA
MCP-Protocol-Version: 2025-06-18
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8

{
  "method": "notifications/initialized",
  "params": {},
  "jsonrpc": "2.0"
}

这个请求在HTTP头中带上了上一步获取的Mcp-Session-Id和双方商定的MCP-Protocol-Version

服务器收到后,会再次返回一个SSE响应,内容与第一次类似,标志着握手完成,会话正式建立。

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 01:57:01 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7__UKHVt_9PM30T9KfWqDcHrSQUy3f34bIXzMKW-r2xhMrKclIQijzdY8FIWAAMLXnHVpWepSgNmZ02LKSIgsThMwffivlsALrlt_5PBExlLXRZo59M7NL3sDmWf22zTLPGymVcJHKk_lPOvSxV5ClxspnQbKLx-XgqPCAU6yt6D2E060A-fJoZ_vYNqpYe08bXkTvOdsvCrbweWBcsdL1cABx5jwfypX1CuZkcbuTUA

event: message
data: {"result":{"protocolVersion":"2025-06-18","capabilities":{"logging":{},"tools":{"listChanged":true}},"serverInfo":{"name":"LINQPad.ScriptHost","version":"1.0.0.0"}},"id":1,"jsonrpc":"2.0"}

深度解析:为何需要两步初始化?

你可能会问,为什么设计如此复杂的两步初始化过程?

根本原因在于,这是一个健壮且灵活的协议设计模式,其核心思想是分离“协商”与“确认”。这确保了客户端和服务器在开始正式数据交换前,就所有关键参数(如协议版本、会话ID、双方能力等)达成完全一致。

  • 第一步 initialize (协商阶段):客户端发起提议,服务器响应提议、确定通信参数并创建会话,返回会话ID(Mcp-Session-Id)。此时,双方只是达成了“如何沟通”的共识。
  • 第二步 initialized (确认阶段):客户端使用会话ID和协议版本发起确认,告诉服务器:“我已经收到你的响应,并准备好按商定的规则开始通信了。”

这种设计的必要性体现在:

  1. 避免竞态条件 (Race Conditions):如果没有第二步确认,客户端可能在收到initialize响应后立即发送业务请求,但此时服务器可能还未完全准备好。第二步就像一个明确的同步信号。
  2. 保证状态一致性:类似TCP的三次握手,这种模式确保了通信双方对会话状态的认知完全一致,为后续的稳定通信奠定基础。
  3. 灵活性和扩展性:该设计允许在协商阶段加入更复杂的逻辑。例如,服务器可以要求客户端在确认前完成某些额外设置。

简单类比一下,这就像一个正式的电话会议:

  1. 第一步 (initialize): 你打电话:“你好,我是张三,能现在开会讨论项目A吗?” 对方回答:“可以,我是李四。我们就用中文讨论,会议号是12345。”
  2. 第二步 (initialized): 你说:“好的,收到,会议号12345,我们正式开始吧。”

没有第二步,对方就无法确定你是否已准备就绪。总之,MCP通过两步初始化,实现了一个可靠、同步且灵活的握手过程

第二部分:方法确认 (GET请求)

在初始化完成后,SDK可能会尝试发送一个GET请求来确认连接。

GET / HTTP/1.1
Host: localhost:5000
Accept: text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7__UKHVt_9PM30T9KfWqDcHrSQUy3f34bIXzMKW-r2xhMrKclIQijzdY8FIWAAMLXnHVpWepSgNmZ02LKSIgsThMwffivlsALrlt_5PBExlLXRZo59M7NL3sDmWf22zTLPGymVcJHKk_lPOvSxV5ClxspnQbKLx-XgqPCAU6yt6D2E060A-fJoZ_vYNqpYe08bXkTvOdsvCrbweWBcsdL1cABx5jwfypX1CuZkcbuTUA
MCP-Protocol-Version: 2025-06-18

然而,在我使用的 ModelContextProtocol.AspNetCore 0.3.0-preview.3 版本中,服务端并未实现对GET请求的处理逻辑。因此,服务器返回了 HTTP 405 Method Not Allowed,并在 Allow 头中明确指出只支持 POST

HTTP/1.1 405 Method Not Allowed
Content-Length: 0
Date: Mon, 21 Jul 2025 01:57:01 GMT
Server: Kestrel
Allow: POST

第三部分:正常通信

握手成功后,客户端和服务端就可以开始真正的数据交换了。所有业务请求都通过POST方法进行。

1. 列出可用工具

首先,我们发送一个 tools/list 请求来获取服务端提供的所有工具。

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7__UKHVt_9PM30T9KfWqDcHrSQUy3f34bIXzMKW-r2xhMrKclIQijzdY8FIWAAMLXnHVpWepSgNmZ02LKSIgsThMwffivlsALrlt_5PBExlLXRZo59M7NL3sDmWf22zTLPGymVcJHKk_lPOvSxV5ClxspnQbKLx-XgqPCAU6yt6D2E060A-fJoZ_vYNqpYe08bXkTvOdsvCrbweWBcsdL1cABx5jwfypX1CuZkcbuTUA
MCP-Protocol-Version: 2025-06-18
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8

{
  "method": "tools/list",
  "params": {},
  "id": 2,
  "jsonrpc": "2.0"
}

服务器返回一个SSE消息,data字段中包含了工具列表的JSON数组,每个工具都有名称、描述和输入参数的Schema。

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 01:57:01 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7__UKHVt_9PM30T9KfWqDcHrSQUy3f34bIXzMKW-r2xhMrKclIQijzdY8FIWAAMLXnHVpWepSgNmZ02LKSIgsThMwffivlsALrlt_5PBExlLXRZo59M7NL3sDmWf22zTLPGymVcJHKk_lPOvSxV5ClxspnQbKLx-XgqPCAU6yt6D2E060A-fJoZ_vYNqpYe08bXkTvOdsvCrbweWBcsdL1cABx5jwfypX1CuZkcbuTUA

event: message
data: {"result":{"tools":[{"name":"echo","description":"Echoes the message back to the client.","inputSchema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}},{"name":"test_throw","description":"Throws an exception for testing purposes.","inputSchema":{"type":"object","properties":{}}},{"name":"count","description":"Counts from 0 to n, reporting progress at each step.","inputSchema":{"type":"object","properties":{"n":{"type":"integer"}},"required":["n"]}},{"name":"echo_ip","description":"Returns the IP address of the client.","inputSchema":{"type":"object","properties":{}}}]},"id":2,"jsonrpc":"2.0"}

2. 调用简单工具 (echo)

接下来,我们调用 echo 工具。请求的 method 为 tools/callparams 中指定了工具名称和参数。

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij52NgZZYYWNIR0QjlQMp-3gHqqQtWdqoCun83RIwOM6LbD-qaJs4wCuiWspDO0LfV39fueDbONZIRWdm8iEsSrFQTAgsgBkxNtsUqlHDtbPvnkFNScCfVtzljqHOc9xfiuxHaBGoLaQJFxWM98Ko9aLy7FcWEeKEOuyvYg7biTtdjyYzyFwZ3ijmP2UBC0mzbP7SrW2Kdu58E1i2MMF3y2p7XHmkaPL6RuOOWSFfwCTeA
MCP-Protocol-Version: 2025-06-18
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8

{
  "method": "tools/call",
  "params": {
    "name": "echo",
    "arguments": {
      "message": ".NET is awesome!"
    }
  },
  "id": 3,
  "jsonrpc": "2.0"
}

服务器返回结果,result.content 字段包含了工具的输出。

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 02:28:19 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij52NgZZYYWNIR0QjlQMp-3gHqqQtWdqoCun83RIwOM6LbD-qaJs4wCuiWspDO0LfV39fueDbONZIRWdm8iEsSrFQTAgsgBkxNtsUqlHDtbPvnkFNScCfVtzljqHOc9xfiuxHaBGoLaQJFxWM98Ko9aLy7FcWEeKEOuyvYg7biTtdjyYzyFwZ3ijmP2UBC0mzbP7SrW2Kdu58E1i2MMF3y2p7XHmkaPL6RuOOWSFfwCTeA

event: message
data: {"result":{"content":[{"type":"text","text":"hello .NET is awesome!"}]},"id":3,"jsonrpc":"2.0"}

3. 调用带进度报告的工具 (count)

MCP的一个强大功能是支持进度报告。我们通过调用 count 工具来演示。注意,请求的params中增加了一个 _meta 字段,其中包含一个客户端生成的 progressToken

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij52NgZZYYWNIR0QjlQMp-3gHqqQtWdqoCun83RIwOM6LbD-qaJs4wCuiWspDO0LfV39fueDbONZIRWdm8iEsSrFQTAgsgBkxNtsUqlHDtbPvnkFNScCfVtzljqHOc9xfiuxHaBGoLaQJFxWM98Ko9aLy7FcWEeKEOuyvYg7biTtdjyYzyFwZ3ijmP2UBC0mzbP7SrW2Kdu58E1i2MMF3y2p7XHmkaPL6RuOOWSFfwCTeA
MCP-Protocol-Version: 2025-06-18
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8

{
  "method": "tools/call",
  "params": {
    "name": "count",
    "arguments": {
      "n": 5
    },
    "_meta": {
      "progressToken": "9021fd27304a48e8ada90e35a66bc1dd"
    }
  },
  "id": 4,
  "jsonrpc": "2.0"
}

这次,服务器的SSE响应是一个事件流。它会陆续发送多个 event: message,其中包含了进度更新。这些进度通知的methodnotifications/progress,并通过 progressToken 与原始请求关联。当任务完成后,最后一条消息才包含最终的result

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 02:28:19 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij52NgZZYYWNIR0QjlQMp-3gHqqQtWdqoCun83RIwOM6LbD-qaJs4wCuiWspDO0LfV39fueDbONZIRWdm8iEsSrFQTAgsgBkxNtsUqlHDtbPvnkFNScCfVtzljqHOc9xfiuxHaBGoLaQJFxWM98Ko9aLy7FcWEeKEOuyvYg7biTtdjyYzyFwZ3ijmP2UBC0mzbP7SrW2Kdu58E1i2MMF3y2p7XHmkaPL6RuOOWSFfwCTeA

event: message
data: {"method":"notifications/progress","params":{"progressToken":"9021fd27304a48e8ada90e35a66bc1dd","progress":0,"total":5,"message":"Step 0 of 5"},"jsonrpc":"2.0"}

event: message
data: {"method":"notifications/progress","params":{"progressToken":"9021fd27304a48e8ada90e35a66bc1dd","progress":1,"total":5,"message":"Step 1 of 5"},"jsonrpc":"2.0"}

event: message
data: {"method":"notifications/progress","params":{"progressToken":"9021fd27304a48e8ada90e35a66bc1dd","progress":2,"total":5,"message":"Step 2 of 5"},"jsonrpc":"2.0"}

event: message
data: {"method":"notifications/progress","params":{"progressToken":"9021fd27304a48e8ada90e35a66bc1dd","progress":3,"total":5,"message":"Step 3 of 5"},"jsonrpc":"2.0"}

event: message
data: {"method":"notifications/progress","params":{"progressToken":"9021fd27304a48e8ada90e35a66bc1dd","progress":4,"total":5,"message":"Step 4 of 5"},"jsonrpc":"2.0"}

event: message
data: {"result":{"content":[{"type":"text","text":"5"}]},"id":4,"jsonrpc":"2.0"}

第四部分:异常与错误处理

一个健壮的协议必须能优雅地处理各种意外情况。MCP协议通过两种不同的方式来报告错误,我们通过调用 test_throw(在服务端会主动抛出异常)和调用一个不存在的工具 not-existing-tool 来观察这两种机制。

4. 工具执行时抛出异常 (test_throw)

现在,我们调用那个被设计为一定会失败的 test_throw 工具。

请求 (Request)

请求本身与调用普通工具无异,它遵循标准的 tools/call 格式。

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7KnxH5A76vXaHfcu5WUlT2qwOcZKw7FC0F8iyfmDU4-weDzJNcH1AjCirhnrqpCjAXI52umwTrb8y7K4rEnuC-l89Frm26vXrg06cNEySoeTevw6g_SYt7fJRu-1vb3OprOeeUjJMQUJH4v5sf__UMpAkO9caBvjxc1Qiqko2Fy0UiB_gCq0jsTQ_keGq_kfDqD9LUSj41LLfUboRlnln4_xWhQ8jLmbNvDiR5F6B9LA
MCP-Protocol-Version: 2025-06-18
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8

{
  "method": "tools/call",
  "params": {
    "name": "test_throw"
  },
  "id": 5,
  "jsonrpc": "2.0"
}

响应 (Response)

这是有趣的地方。服务器返回的HTTP状态码依然是 200 OK,表示HTTP通信本身是成功的。然而,响应体内的JSON-RPC报文揭示了真实情况。

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 03:17:20 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7KnxH5A76vXaHfcu5WUlT2qwOcZKw7FC0F8iyfmDU4-weDzJNcH1AjCirhnrqpCjAXI52umwTrb8y7K4rEnuC-l89Frm26vXrg06cNEySoeTevw6g_SYt7fJRu-1vb3OprOeeUjJMQUJH4v5sf__UMpAkO9caBvjxc1Qiqko2Fy0UiB_gCq0jsTQ_keGq_kfDqD9LUSj41LLfUboRlnln4_xWhQ8jLmbNvDiR5F6B9LA

event: message
data: {"result":{"content":[{"type":"text","text":"An error occurred invoking 'test_throw'."}],"isError":true},"id":5,"jsonrpc":"2.0"}

深度解析:

请注意,JSON-RPC报文返回的不是一个顶级的 error 对象,而是一个 result 对象。这说明从JSON-RPC协议的层面来看,这次调用是“成功”的。但是,result 对象内部增加了一个关键字段:"isError": true

这是一种精巧的设计:它区分了协议层面的错误业务逻辑层面的错误

  • 协议层面:客户端的请求格式正确,服务器也找到了名为 test_throw 的工具并成功尝试执行它。因此,JSON-RPC的交互流程是完整的。
  • 业务逻辑层面:工具在执行期间内部发生了未捕获的异常。MCP服务端捕获了这个异常,并将其封装成一个“错误结果”返回。isError: true 就是一个明确的信号,告诉客户端:“我尝试执行了,但工具自己出错了”。

这种方式让客户端可以统一处理所有 tools/call 的响应,然后通过检查 isError 标志来判断工具的执行是否真正成功。

5. 调用不存在的工具

接下来,我们尝试调用一个从未在服务端定义过的工具:not-existing-tool

请求 (Request)

请求结构依然是标准的 tools/call

POST / HTTP/1.1
Host: localhost:5000
Accept: application/json, text/event-stream
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7KnxH5A76vXaHfcu5WUlT2qwOcZKw7FC0F8iyfmDU4-weDzJNcH1AjCirhnrqpCjAXI52umwTrb8y7K4rEnuC-l89Frm26vXrg06cNEySoeTevw6g_SYt7fJRu-1vb3OprOeeUjJMQUJH4v5sf__UMpAkO9caBvjxc1Qiqko2Fy0UiB_gCq0jsTQ_keGq_kfDqD9LUSj41LLfUboRlnln4_xWhQ8jLmbNvDiR5F6B9LA
MCP-Protocol-Version: 2025-06-18
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8

{
  "method": "tools/call",
  "params": {
    "name": "not-existing-tool"
  },
  "id": 6,
  "jsonrpc": "2.0"
}

响应 (Response)

这次的响应与上一个场景截然不同。

HTTP/1.1 200 OK
Content-Type: text/event-stream
Date: Mon, 21 Jul 2025 03:17:20 GMT
Server: Kestrel
Cache-Control: no-cache,no-store
Content-Encoding: identity
Transfer-Encoding: chunked
Mcp-Session-Id: CfDJ8PaF_EVr9adHn4ULGk-4ij7KnxH5A76vXaHfcu5WUlT2qwOcZKw7FC0F8iyfmDU4-weDzJNcH1AjCirhnrqpCjAXI52umwTrb8y7K4rEnuC-l89Frm26vXrg06cNEySoeTevw6g_SYt7fJRu-1vb3OprOeeUjJMQUJH4v5sf__UMpAkO9caBvjxc1Qiqko2Fy0UiB_gCq0jsTQ_keGq_kfDqD9LUSj41LLfUboRlnln4_xWhQ8jLmbNvDiR5F6B9LA

event: message
data: {"error":{"code":-32602,"message":"Unknown tool: 'not-existing-tool'"},"id":6,"jsonrpc":"2.0"}

深度解析:

看到区别了吗?这次的响应体直接包含了一个顶级的 error 对象,完全符合JSON-RPC 2.0的错误响应规范。

  • "code": -32602:这是JSON-RPC的一个标准错误码,代表 “Invalid params” (无效参数)。在这里,服务端认为tools/call方法中的name参数值"not-existing-tool"是无效的,因为找不到对应的工具。
  • "message": "Unknown tool: 'not-existing-tool'":提供了人类可读的错误描述。

这被视为一个协议层面的错误,因为客户端请求执行一个从服务器视角看根本不存在的方法(或资源)。服务器甚至都无法开始“执行工具”这个业务逻辑,因为它在第一步——查找工具时——就失败了。因此,它直接返回一个标准的JSON-RPC错误,终止了这次调用。

总结

通过以上的抓包分析,我们可以清晰地总结出(无状态)MCP协议的核心通信模式:

  1. 协议基础:MCP构建在 JSON-RPC 2.0 之上,通过 HTTP POST 请求进行交互。
  2. 会话管理:通过一个健壮的两步握手initialize 和 initialized)来建立会话,并使用 Mcp-Session-Id HTTP头来标识和维持该会话。
  3. 响应机制:服务端使用 Server-Sent Events (SSE) (Content-Type: text/event-stream) 来响应客户端。这种方式天然支持流式数据,非常适合长任务的进度报告。
  4. 数据格式:无论是请求的body还是SSE返回的data部分,都遵循JSON-RPC 2.0的报文结构 ({"jsonrpc": "2.0", "method": "...", "params": ..., "id": ...} 或 {"jsonrpc": "2.0", "result": ..., "id": ...})。
  5. 错误处理:MCP协议区分了两种错误。协议层面的错误(如调用不存在的工具)会返回标准的JSON-RPC error对象。而工具执行期间的业务逻辑错误则通过在成功的 result 对象中附加 isError: true 标志来表示,实现了协议与业务的分离。

希望本文能帮助你彻底搞懂MCP的底层通信原理。掌握了这些,你不仅能更好地使用官方SDK,甚至可以在不支持的语言或环境中实现自己的MCP客户端或服务端。


感谢您的阅读,如果您有任何问题或想法,欢迎在评论区留言讨论。

也欢迎加入我们的 .NET骚操作 QQ群一起探讨:495782587

C#AES CBC、PKCS7加密、解密_paddingmode.pkcs7-CSDN博客

mikel阅读(268)

来源: C#AES CBC、PKCS7加密、解密_paddingmode.pkcs7-CSDN博客

C#开发中,遇到AES加密,解密问题,因此,在这里记录一下,

该方法默认以下模式:

Mode:CBC

Padding:PKCS7

方法支持:

1.AES加密,返回base64的密文

string msg = AESHelper.AESEncrypt(“待加密内容”,”key”,”v”);

2.AES解密,返回utf-8的明文

string msg = AESHelper.AESEncrypt2(“待加密内容”,”key”,”v”);

代码类如下:

/// <summary>
/// AES 加密、解密
/// </summary>
public class AESHelper
{

#region AES加密,返回base64的密文
/// <summary>
/// <para>Describe:AES加密,返回base64的密文</para>
/// <para>Author:KAI</para>
/// </summary>
/// <param name=”toEncrypt”>源数据</param>
/// <param name=”key”>密钥</param>
/// <param name=”iv”>向量</param>
/// <returns>base64的密文</returns>
public static string AESEncrypt(string toEncrypt, string key, string iv)
{
byte[] keyArray = ConvertToByteAndAppend0(key, 24);
byte[] ivArray = ConvertToByteAndAppend0(iv, 16);

byte[] toEncryptArray = UTF8Encoding.UTF8.GetBytes(toEncrypt);
RijndaelManaged rDel = new RijndaelManaged();
rDel.Key = keyArray;
rDel.IV = ivArray;
rDel.Mode = CipherMode.CBC;
rDel.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = rDel.CreateEncryptor();
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
return Convert.ToBase64String(resultArray, 0, resultArray.Length);
}
public static byte[] ConvertToByteAndAppend0(string value, int intLength)
{
byte[] valueArray = new byte[intLength];
byte[] temp = UTF8Encoding.UTF8.GetBytes(value);
for (int i = 0; i < temp.Length; i++)
{
valueArray[i] = temp[i];
}
for (int i = temp.Length; i < intLength; i++)
{
valueArray[i] = 0x0;
}
return valueArray;
}
#endregion

#region AES解密,返回utf-8的明文
/// <summary>
/// <para>Describe:AES解密,返回utf-8的明文</para>
/// <para>Author:KAI</para>
/// </summary>
/// <param name=”toEncrypt”>密文</param>
/// <param name=”key”>密钥</param>
/// <param name=”iv”>向量</param>
/// <returns>utf-8的明文</returns>
public static string AESEncrypt2(string toDecrypt, string key, string iv)
{
byte[] keyArray = ConvertToByteAndAppend0(key, 24);
byte[] ivArray = ConvertToByteAndAppend0(iv, 16);

byte[] toEncryptArray = Convert.FromBase64String(toDecrypt);
RijndaelManaged rDel = new RijndaelManaged();
rDel.Key = keyArray;
rDel.IV = ivArray;
rDel.Mode = CipherMode.CBC;
rDel.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = rDel.CreateDecryptor();
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
string f = UTF8Encoding.UTF8.GetString(resultArray);
return UTF8Encoding.UTF8.GetString(resultArray);
}
#endregion
#region AES解密
/// <summary>
/// 解密
/// </summary>
/// <param name=”encryptResultStr”>待解密的密文字</param>
/// <param name=”decryptKey”>解密的key</param>
/// <returns>解密后的字符串</returns>
public static string AesDecrypt(String encryptResultStr, String decryptKey)
{
//base64 decode
String decrpt = EbotongDecrypto(encryptResultStr);
//hexStr to Str
byte[] decryptFrom = ParseHexStr2Str(decrpt);
//
return decrypt(decryptFrom, decryptKey);
}
/// <summary>
/// Base64解密
/// </summary>
/// <param name=”result”>待解密的密文</param>
/// <returns>解密后的字符串</returns>
public static string EbotongDecrypto(string result)
{
string decode = “”;
byte[] bytes = Convert.FromBase64String(result);
try
{
decode = Encoding.UTF8.GetString(bytes);
}
catch
{
decode = result;
}
return decode;
}

/// <summary>
/// 16进制字符串转字符串
/// </summary>
/// <param name=”hexStr”>16进制字符串</param>
/// <returns>转换后的字符串</returns>
public static byte[] ParseHexStr2Str(String hexStr)
{
if (hexStr == null || hexStr.Length < 1)
return null;
byte[] result = new byte[hexStr.Length / 2];
String s = “”;
for (int i = 0; i < hexStr.Length / 2; i++)
{
s = hexStr.Substring(i * 2, 1);
int high = Convert.ToInt32(s, 16);
s = hexStr.Substring(i * 2 + 1, 1);
int low = Convert.ToInt32(s, 16);
result[i] = (byte)(high * 16 + low);
}
return result;
}

/// <summary>
/// 解密
/// </summary>
/// <param name=”toDecrypt”>待解密的密文字节数组</param>
/// <param name=”key”>解密的key</param>
/// <returns>解密后的字符串</returns>
public static string decrypt(byte[] toDecrypt, string key)
{
//需要注意几点:
//1)C#默认运算模式为CBC,java默认为ECB,因此要将C#的加密方式改为ECB
//2)C#的Padding方式要设置为PaddingMode.PKCS7,否则解密出来后结尾可能有乱码
//key = “cGh19CbAIehVxt5ZqRDBJw==”;
byte[] keyArray = Convert.FromBase64String(key);
RijndaelManaged rDel = new RijndaelManaged();
rDel.Key = keyArray;
rDel.Mode = CipherMode.ECB;//必须设置为ECB
rDel.Padding = PaddingMode.PKCS7;//必须设置为PKCS7
ICryptoTransform cTransform = rDel.CreateDecryptor();
byte[] resultArray = cTransform.TransformFinalBlock(toDecrypt, 0, toDecrypt.Length);
return UTF8Encoding.UTF8.GetString(resultArray);
}
#endregion
}
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/qq_35052673/article/details/134811853

c# 模拟Http请求 - Ysの陈憨憨 - 博客园

mikel阅读(409)

来源: c# 模拟Http请求 – Ysの陈憨憨 – 博客园

 

一、使用HttpClient

适用的.NET Framework 版本包括:.NET Framework 4.5+, .NET Standard 1.1+, .NET Core 1.0+
你可以通过Nuget:https://www.nuget.org/packages/Microsoft.Net.Http来安装

1.1 添加引用

using System.Net.Http;

1.2 实例化

建议在你应用程序的生命周期中使用同一个 HttpClient的实例

private static readonly HttpClient client = new HttpClient();

1.3 POST发送请求

var values = new Dictionary<string, string>
{
   { "thing1", "hello" },
   { "thing2", "world" }
};
var content = new FormUrlEncodedContent(values);
var response = await client.PostAsync("http://www.example.com/recepticle.aspx", content);
var responseString = await response.Content.ReadAsStringAsync();

1.4 GET发送请求

var responseString = await client.GetStringAsync("http://www.example.com/recepticle.aspx");

 

二、使用RestSharp

可以通过Nuget:https://www.nuget.org/packages/RestSharp来安装,具体使用示例:

2.1 发送请求

var client = new RestClient("http://example.com");
// client.Authenticator = new HttpBasicAuthenticator(username, password);

var request = new RestRequest("resource/{id}", Method.POST);
request.AddParameter("name", "value"); // adds to POST or URL querystring based on Method
request.AddUrlSegment("id", "123"); // replaces matching token in request.Resource

// easily add HTTP Headers
request.AddHeader("header", "value");

// add files to upload (works with compatible verbs)
request.AddFile(path);

// execute the request
IRestResponse response = client.Execute(request);
var content = response.Content; // raw content as string

// or automatically deserialize result
// return content type is sniffed but can be explicitly set via RestClient.AddHandler();
RestResponse<Person> response2 = client.Execute<Person>(request);
var name = response2.Data.Name;

// easy async support
client.ExecuteAsync(request, response => {
    Console.WriteLine(response.Content);
});

// async with deserialization
var asyncHandle = client.ExecuteAsync<Person>(request, response => {
    Console.WriteLine(response.Data.Name);
});

// abort the request on demand
asyncHandle.Abort();

 

三、使用Flurl.Http

3.1 Nuget安装

Nuget地址:https://www.nuget.org/packages/Flurl.Http

using Flurl.Http;

3.2 POST发送请求

var responseString = await "http://www.example.com/recepticle.aspx"
    .PostUrlEncodedAsync(new { thing1 = "hello", thing2 = "world" })
    .ReceiveString();

3.3 GET发送请求

var responseString = await "http://www.example.com/recepticle.aspx"
    .GetStringAsync();

 

四、使用HttpWebRequest

4.1 引入命名空间

适用的.NET framework: .NET Framework 1.1+, .NET Standard 2.0+, .NET Core 1.0+

using System.Net;
using System.Text;  // for class Encoding
using System.IO;    // for StreamReader

4.2 POST发送请求

var request = (HttpWebRequest)WebRequest.Create("http://www.example.com/recepticle.aspx");
var postData = "thing1=hello";
    postData += "&thing2=world";
var data = Encoding.ASCII.GetBytes(postData);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.ContentLength = data.Length;
using (var stream = request.GetRequestStream())
{
    stream.Write(data, 0, data.Length);
}
var response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();

4.3 GET发送请求

var request = (HttpWebRequest)WebRequest.Create("http://www.example.com/recepticle.aspx");
var response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();

 

五、使用WebClient

5.1 引入命名空间

适用的.NET framework: .NET Framework 1.1+, .NET Standard 2.0+, .NET Core 1.0+

using System.Net;
using System.Collections.Specialized;

5.2 Post发送请求

using (var client = new WebClient())
{
    var values = new NameValueCollection();
    values["thing1"] = "hello";
    values["thing2"] = "world";
    var response = client.UploadValues("http://www.example.com/recepticle.aspx", values);
    var responseString = Encoding.Default.GetString(response);
}

//或者
using (var client = new WebClient())
{
    byte[] dataToPost = Encoding.Default.GetBytes("thing1=hello&thing2=world");
    client.Header.Add("Content-Type","application/x-www-form-urlencoded");  //采用Post必须要加的Header,如果改成GET的话可以除去
    byte[] response = client.UploadData("http://example.com""POST",dataToPost);
    string strResult = Encoding.UTF8.GetString(response);
}

5.3 GET发送请求

using (var client = new WebClient())
{
    var responseString = client.DownloadString("http://www.example.com/recepticle.aspx");
}

使用Restsharp实现http请求 - Simon.lu - 博客园

mikel阅读(216)

来源: 使用Restsharp实现http请求 – Simon.lu – 博客园

之前使用Postman 的时候 发现 自动生成的code 里面 的C# 代码是 使用的 Restsharp。

今天用要做http 请求,去调用其他接口,就决定使用 Restsharp 实现。

restsharp 官网 : http://restsharp.org

 

首先 在nuget 安装依赖包, Install-Package RestSharp -Version 106.2.2

 

贴一下官方的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var client = new RestClient("http://example.com");
// client.Authenticator = new HttpBasicAuthenticator(username, password);
var request = new RestRequest("resource/{id}", Method.POST);
request.AddParameter("name""value"); // adds to POST or URL querystring based on Method
request.AddUrlSegment("id""123"); // replaces matching token in request.Resource
// easily add HTTP Headers
request.AddHeader("header""value");
// add files to upload (works with compatible verbs)
request.AddFile(path);
// execute the request
IRestResponse response = client.Execute(request);
var content = response.Content; // raw content as string
// or automatically deserialize result
// return content type is sniffed but can be explicitly set via RestClient.AddHandler();
RestResponse<Person> response2 = client.Execute<Person>(request);
var name = response2.Data.Name;
// easy async support
client.ExecuteAsync(request, response => {
    Console.WriteLine(response.Content);
});
// async with deserialization
var asyncHandle = client.ExecuteAsync<Person>(request, response => {
    Console.WriteLine(response.Data.Name);
});
// abort the request on demand
asyncHandle.Abort();

我去撸代码了,也许会有补充……

C#实现AES加密解密 - huanglg - 博客园

mikel阅读(401)

来源: C#实现AES加密解密 – huanglg – 博客园

AES 高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法

Rijndael(读作rain-dahl)是由美国国家标准与技术协会(NIST)所选的高级加密标准(AES)的候选算法。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。

Rijndael 算法首先是一个密钥分组加密的算法,通过置换(permutations )和替换(substitutions)迭代加密,进过多轮操作形成密文。

AES算是Rijndael算法的一种特殊实现,选的分组为128bit(16字节),密钥可以使用128、192 和 256bit三种,而Rijndael使用的密钥和区块长度可以是32位的整数倍,以128位为下限,256比特为上限。加密过程中使用的密钥是由Rijndael密钥生成方案产生。

AES加密过程是在一个4×4的字节矩阵上运作,这个矩阵又称为“状态(state)”,其初值就是一个明文区块(矩阵中一个元素大小就是明文区块中的一个Byte)。(Rijndael加密法因支持更大的区块,其矩阵行数可视情况增加)加密时,各轮AES加密循环(除最后一轮外)均包含4个步骤:
AddRoundKey — 矩阵中的每一个字节都与该次轮秘钥(round key)做XOR运算;每个子密钥由密钥生成方案产生。
SubBytes — 通过非线性的替换函数,用查找表的方式把每个字节替换成对应的字节。
ShiftRows — 将矩阵中的每个横列进行循环式移位。
MixColumns — 为了充分混合矩阵中各个直行的操作。这个步骤使用线性转换来混合每列的四个字节。

 

 

RijndaelManager代码实现

 

 

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.Security.Cryptography;
  5. using System.IO;
  6. namespace Csharp
  7. {
  8. class AESHelper
  9. {
  10. /// <summary>
  11. /// AES加密
  12. /// </summary>
  13. /// <param name=”Data”>被加密的明文</param>
  14. /// <param name=”Key”>密钥</param>
  15. /// <param name=”Vector”>向量</param>
  16. /// <returns>密文</returns>
  17. public static String AESEncrypt(String Data, String Key, String Vector)
  18. {
  19. Byte[] plainBytes = Encoding.UTF8.GetBytes(Data);
  20. Byte[] bKey = new Byte[32];
  21. Array.Copy(Encoding.UTF8.GetBytes(Key.PadRight(bKey.Length)), bKey, bKey.Length);
  22. Byte[] bVector = new Byte[16];
  23. Array.Copy(Encoding.UTF8.GetBytes(Vector.PadRight(bVector.Length)), bVector, bVector.Length);
  24. Byte[] Cryptograph = null// 加密后的密文
  25. Rijndael Aes = Rijndael.Create();
  26. try
  27. {
  28. // 开辟一块内存流
  29. using (MemoryStream Memory = new MemoryStream())
  30. {
  31. // 把内存流对象包装成加密流对象
  32. using (CryptoStream Encryptor = new CryptoStream(Memory,
  33. Aes.CreateEncryptor(bKey, bVector),
  34. CryptoStreamMode.Write))
  35. {
  36. // 明文数据写入加密流
  37. Encryptor.Write(plainBytes, 0, plainBytes.Length);
  38. Encryptor.FlushFinalBlock();
  39. Cryptograph = Memory.ToArray();
  40. }
  41. }
  42. }
  43. catch
  44. {
  45. Cryptograph = null;
  46. }
  47. return Convert.ToBase64String(Cryptograph);
  48. }
  49. /// <summary>
  50. /// AES解密
  51. /// </summary>
  52. /// <param name=”Data”>被解密的密文</param>
  53. /// <param name=”Key”>密钥</param>
  54. /// <param name=”Vector”>向量</param>
  55. /// <returns>明文</returns>
  56. public static String AESDecrypt(String Data, String Key, String Vector)
  57. {
  58. Byte[] encryptedBytes = Convert.FromBase64String(Data);
  59. Byte[] bKey = new Byte[32];
  60. Array.Copy(Encoding.UTF8.GetBytes(Key.PadRight(bKey.Length)), bKey, bKey.Length);
  61. Byte[] bVector = new Byte[16];
  62. Array.Copy(Encoding.UTF8.GetBytes(Vector.PadRight(bVector.Length)), bVector, bVector.Length);
  63. Byte[] original = null// 解密后的明文
  64. Rijndael Aes = Rijndael.Create();
  65. try
  66. {
  67. // 开辟一块内存流,存储密文
  68. using (MemoryStream Memory = new MemoryStream(encryptedBytes))
  69. {
  70. // 把内存流对象包装成加密流对象
  71. using (CryptoStream Decryptor = new CryptoStream(Memory,
  72. Aes.CreateDecryptor(bKey, bVector),
  73. CryptoStreamMode.Read))
  74. {
  75. // 明文存储区
  76. using (MemoryStream originalMemory = new MemoryStream())
  77. {
  78. Byte[] Buffer = new Byte[1024];
  79. Int32 readBytes = 0;
  80. while ((readBytes = Decryptor.Read(Buffer, 0, Buffer.Length)) > 0)
  81. {
  82. originalMemory.Write(Buffer, 0, readBytes);
  83. }
  84. original = originalMemory.ToArray();
  85. }
  86. }
  87. }
  88. }
  89. catch
  90. {
  91. original = null;
  92. }
  93. return Encoding.UTF8.GetString(original);
  94. }
  95. /// <summary>
  96. /// AES加密(无向量)
  97. /// </summary>
  98. /// <param name=”plainBytes”>被加密的明文</param>
  99. /// <param name=”key”>密钥</param>
  100. /// <returns>密文</returns>
  101. public static string AESEncrypt(String Data, String Key)
  102. {
  103. MemoryStream mStream = new MemoryStream();
  104. RijndaelManaged aes = new RijndaelManaged();
  105. byte[] plainBytes = Encoding.UTF8.GetBytes(Data);
  106. Byte[] bKey = new Byte[32];
  107. Array.Copy(Encoding.UTF8.GetBytes(Key.PadRight(bKey.Length)), bKey, bKey.Length);
  108. aes.Mode = CipherMode.ECB;
  109. aes.Padding = PaddingMode.PKCS7;
  110. aes.KeySize = 128;
  111. //aes.Key = _key;
  112. aes.Key = bKey;
  113. //aes.IV = _iV;
  114. CryptoStream cryptoStream = new CryptoStream(mStream, aes.CreateEncryptor(), CryptoStreamMode.Write);
  115. try
  116. {
  117. cryptoStream.Write(plainBytes, 0, plainBytes.Length);
  118. cryptoStream.FlushFinalBlock();
  119. return Convert.ToBase64String(mStream.ToArray());
  120. }
  121. finally
  122. {
  123. cryptoStream.Close();
  124. mStream.Close();
  125. aes.Clear();
  126. }
  127. }
  128. /// <summary>
  129. /// AES解密(无向量)
  130. /// </summary>
  131. /// <param name=”encryptedBytes”>被加密的明文</param>
  132. /// <param name=”key”>密钥</param>
  133. /// <returns>明文</returns>
  134. public static string AESDecrypt(String Data, String Key)
  135. {
  136. Byte[] encryptedBytes = Convert.FromBase64String(Data);
  137. Byte[] bKey = new Byte[32];
  138. Array.Copy(Encoding.UTF8.GetBytes(Key.PadRight(bKey.Length)), bKey, bKey.Length);
  139. MemoryStream mStream = new MemoryStream(encryptedBytes);
  140. //mStream.Write( encryptedBytes, 0, encryptedBytes.Length );
  141. //mStream.Seek( 0, SeekOrigin.Begin );
  142. RijndaelManaged aes = new RijndaelManaged();
  143. aes.Mode = CipherMode.ECB;
  144. aes.Padding = PaddingMode.PKCS7;
  145. aes.KeySize = 128;
  146. aes.Key = bKey;
  147. //aes.IV = _iV;
  148. CryptoStream cryptoStream = new CryptoStream(mStream, aes.CreateDecryptor(), CryptoStreamMode.Read);
  149. try
  150. {
  151. byte[] tmp = new byte[encryptedBytes.Length + 32];
  152. int len = cryptoStream.Read(tmp, 0, encryptedBytes.Length + 32);
  153. byte[] ret = new byte[len];
  154. Array.Copy(tmp, 0, ret, 0, len);
  155. return Encoding.UTF8.GetString(ret);
  156. }
  157. finally
  158. {
  159. cryptoStream.Close();
  160. mStream.Close();
  161. aes.Clear();
  162. }
  163. }
  164. }
  165. }

 

AesManager代码实现

 

  1. using System;
  2. using System.IO;
  3. using System.Security.Cryptography;
  4. namespace Aes_Example
  5. {
  6. class AesExample
  7. {
  8. public static void Main()
  9. {
  10. try
  11. {
  12. string original = “Here is some data to encrypt!”;
  13. // Create a new instance of the AesManaged
  14. // class. This generates a new key and initialization
  15. // vector (IV).
  16. using (AesManaged myAes = new AesManaged())
  17. {
  18. // Encrypt the string to an array of bytes.
  19. byte[] encrypted = EncryptStringToBytes_Aes(original, myAes.Key, myAes.IV);
  20. // Decrypt the bytes to a string.
  21. string roundtrip = DecryptStringFromBytes_Aes(encrypted, myAes.Key, myAes.IV);
  22. //Display the original data and the decrypted data.
  23. Console.WriteLine(“Original: {0}”, original);
  24. Console.WriteLine(“Round Trip: {0}”, roundtrip);
  25. }
  26. }
  27. catch (Exception e)
  28. {
  29. Console.WriteLine(“Error: {0}”, e.Message);
  30. }
  31. }
  32. static byte[] EncryptStringToBytes_Aes(string plainText, byte[] Key, byte[] IV)
  33. {
  34. // Check arguments.
  35. if (plainText == null || plainText.Length <= 0)
  36. throw new ArgumentNullException(“plainText”);
  37. if (Key == null || Key.Length <= 0)
  38. throw new ArgumentNullException(“Key”);
  39. if (IV == null || IV.Length <= 0)
  40. throw new ArgumentNullException(“IV”);
  41. byte[] encrypted;
  42. // Create an AesManaged object
  43. // with the specified key and IV.
  44. using (AesManaged aesAlg = new AesManaged())
  45. {
  46. aesAlg.Key = Key;
  47. aesAlg.IV = IV;
  48. // Create a decrytor to perform the stream transform.
  49. ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
  50. // Create the streams used for encryption.
  51. using (MemoryStream msEncrypt = new MemoryStream())
  52. {
  53. using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
  54. {
  55. using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
  56. {
  57. //Write all data to the stream.
  58. swEncrypt.Write(plainText);
  59. }
  60. encrypted = msEncrypt.ToArray();
  61. }
  62. }
  63. }
  64. // Return the encrypted bytes from the memory stream.
  65. return encrypted;
  66. }
  67. static string DecryptStringFromBytes_Aes(byte[] cipherText, byte[] Key, byte[] IV)
  68. {
  69. // Check arguments.
  70. if (cipherText == null || cipherText.Length <= 0)
  71. throw new ArgumentNullException(“cipherText”);
  72. if (Key == null || Key.Length <= 0)
  73. throw new ArgumentNullException(“Key”);
  74. if (IV == null || IV.Length <= 0)
  75. throw new ArgumentNullException(“IV”);
  76. // Declare the string used to hold
  77. // the decrypted text.
  78. string plaintext = null;
  79. // Create an AesManaged object
  80. // with the specified key and IV.
  81. using (AesManaged aesAlg = new AesManaged())
  82. {
  83. aesAlg.Key = Key;
  84. aesAlg.IV = IV;
  85. // Create a decrytor to perform the stream transform.
  86. ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
  87. // Create the streams used for decryption.
  88. using (MemoryStream msDecrypt = new MemoryStream(cipherText))
  89. {
  90. using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
  91. {
  92. using (StreamReader srDecrypt = new StreamReader(csDecrypt))
  93. {
  94. // Read the decrypted bytes from the decrypting stream
  95. // and place them in a string.
  96. plaintext = srDecrypt.ReadToEnd();
  97. }
  98. }
  99. }
  100. }
  101. return plaintext;
  102. }
  103. }
  104. }
  105.  

VUE前后端分离 跨域访问后端接口 宝塔Nginx设置反向代理_vue前后端分离 跨域访问后端接口 宝塔nginx设置代理-CSDN博客

mikel阅读(246)

来源: VUE前后端分离 跨域访问后端接口 宝塔Nginx设置反向代理_vue前后端分离 跨域访问后端接口 宝塔nginx设置代理-CSDN博客

VUE打包后生成的文件放到宝塔,能通过IP正常访问
但是调用后端接口的时候还是访问当前的IP
如访问的IP地址是192.168.1.139:8081/login
访问后端的接口是192.168.1.139:8081/api
实际后端接口是192.168.1.139:8199/api
使用宝塔面板的反向代理。
网站设置 里找到反向代理
打开高级功能,里面的代理目录就是后端接口匹配的字符,例如后端接口都是用/api/xxx的形式,就填/api
目标URL填实际后端的接口地址,也可以是本地的127.0.0.1
具体配置:
#PROXY-START/
# 所有静态请求都由nginx处理,存放目录为/www/wwwroot/hat.com
location ~ \.(htm|html|js|css|jpg|png|gif|eot|svg|ttf|woff|woff2)$ {
root    /www/wwwroot/hat.com;
}
location /api
{
    proxy_pass http://127.0.0.1:8199;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    add_header X-Cache $upstream_cache_status;
    #Set Nginx Cache
    add_header Cache-Control no-cache;
}
#PROXY-END/
AI写代码
bash
————————————————
                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_43924621/article/details/121039944

Vue项目在宝塔面板上的部署流程-CSDN博客

mikel阅读(289)

来源: Vue项目在宝塔面板上的部署流程-CSDN博客

1. Vue项目执行打包命令进行打包得到dist文件夹

npm run build
AI写代码
2. 打开宝塔主页进入网站,选择添加站点

 

3. 将自己的服务器网址作为域名,默认为80端口,可自行设置备注和根目录,点击提交

 

4. 点击设置->配置文件,加入如下代码(配置跨域问题),点击保存(建议手敲,格式很重要)

location /api {
proxy_pass http://gmall-h5-api.atguigu.cn;
}
AI写代码

5. 点击文件,进入前面设置的地址,将原有文件全部删除

 

6. 将前面打包的dist文件夹里面的文件拖到宝塔文件中,进行上传

 

 

7. 进入nginx文件夹下的conf文件夹,找到nginx.conf文件,双击进入编辑,找到server下的listen,将端口改为前面设置的端口,保存即可(需要部署多个项目时复制多个server进行配置)

 

8. 输入前面设置的域名即可进入制作的项目中
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/weixin_56868125/article/details/128935456

【项目配置】vue3项目上传到宝塔面板,图文保姆级教程!_宝塔部署vue3项目-CSDN博客

mikel阅读(255)

来源: 【项目配置】vue3项目上传到宝塔面板,图文保姆级教程!_宝塔部署vue3项目-CSDN博客

前言
这几天在b站上学习vue3项目,视频的后续也没有提到项目打包上传的事情。自己之前在阿里云买了一年服务器,刚好可以用上。

目录

一、项目打包

二、配置宝塔面板

1、新建站名与基本配置

2、配置伪静态代码

3、上传dist文件夹

4、修改网站目录

三、测试

一、项目打包
在终端打开项目目录,打包一下。

 

 

打包成功后会在目录中生成一个dist文件夹

 

二、配置宝塔面板
(跳过登录的环节,这里默认大家登录成功)

1、新建站名与基本配置

 

 

 

 

 

 

 

2、配置伪静态代码
location / {
if (!-e $request_filename) {
rewrite ^(.*)$ /index.html?s=/$1 last;
break;
}
}

AI写代码
JavaScript
运行
3、上传dist文件夹
(找到站点存放文件的位置)

 

 

 

4、修改网站目录

三、测试
(记得是自己真实的公网ip+放行端口)

 

————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/orbit4/article/details/142213726

[云服务器]linux宝塔面板下配置连接mysql的jdbc配置及设置通过自己的域名来访问web项目_jdbc 配置域名访问-CSDN博客

mikel阅读(172)

来源: [云服务器]linux宝塔面板下配置连接mysql的jdbc配置及设置通过自己的域名来访问web项目_jdbc 配置域名访问-CSDN博客

由于之前自己的云服务器在搭建网站的时候就已经安装好了MySQL,所以我对自己服务器的MySQL其实并不怎么熟悉,jdbc一些简单东西也折腾了很久,特记录一些
一.配置连接mySQL的jdbc
首先改下自己jdbc工具类的链接名等,我的是配置文件就改配置文件了,主要需要注意url改成自己服务器的公网IP地址
#mysql properties
driverName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://IP地址:3306/web_test?&useSSL=false&serverTimezone=UTC
user=mizuhokaga
password=1234567
AI写代码
python
运行
1
2
3
4
5
user和password来自宝塔数据库面板不是本地数据库root与root密码!,我们自己新建一个数据库(用原有的还得在phpMyAdmin中改权限),我命名为web_test,注意设置权限:所有人
这是设置用户名和密码就是jdbc配置文件的user和password!
在发布项目之前最好在navicat等工具里进行连接测试,连接成功再发表项
二.设置通过自己的域名来访问web项目
这里我使用的是我的二级域名
①阿里云控制台云解析DNS中添加网站
这个就不多说了
AI写代码
1
②宝塔添加网站并设置反向代理
先宝塔添加网站,之后设置反向代理
AI写代码
1
这样就可以通过域名访问项目了,当然也可以通过ip
————————————————
                            本文为博主原创文章,转载请显著位置标明出处,未经博主允许不得用于商业目的。
原文链接:https://blog.csdn.net/qq_44831907/article/details/104834232